diff --git a/.github/renovate.json b/.github/renovate.json index 71e131e43..42f5f93f2 100644 --- a/.github/renovate.json +++ b/.github/renovate.json @@ -2,48 +2,31 @@ "$schema": "https://docs.renovatebot.com/renovate-schema.json", "extends": [ "config:base", - "schedule:weekends", ":combinePatchMinorReleases", ":dependencyDashboard", ":maintainLockFilesWeekly", ":prConcurrentLimit10", ":rebaseStalePrs", - ":separateMajorReleases" + ":separateMajorReleases", + "group:monorepos", + "schedule:weekends" ], "separateMajorMinor": true, "enabledManagers": ["cargo", "github-actions", "npm", "nuget"], + "commitMessagePrefix": "[deps]:", + "commitMessageTopic": "{{depName}}", "packageRules": [ - { - "groupName": "npm minor", - "matchManagers": ["npm"], - "matchUpdateTypes": ["minor", "patch"] - }, - { - "matchManagers": ["cargo"], - "matchUpdateTypes": ["patch"], - "enabled": false - }, - { - "matchManagers": ["cargo"], - "matchUpdateTypes": ["minor"], - "matchCurrentVersion": ">=1.0.0", - "enabled": false - }, { "matchManagers": ["cargo"], "matchPackagePatterns": ["pyo3*"], "matchUpdateTypes": ["minor", "patch"], "groupName": "pyo3 non-major" }, - { - "groupName": "nuget minor", - "matchManagers": ["nuget"], - "matchUpdateTypes": ["minor", "patch"] - }, { "groupName": "gh minor", "matchManagers": ["github-actions"], "matchUpdateTypes": ["minor", "patch"] } - ] + ], + "ignoreDeps": ["dotnet-sdk"] } diff --git a/.github/workflows/build-android.yml b/.github/workflows/build-android.yml index 110e3758b..d04d15a45 100644 --- a/.github/workflows/build-android.yml +++ b/.github/workflows/build-android.yml @@ -102,7 +102,7 @@ jobs: run: ./build-schemas.sh - name: Publish - uses: gradle/gradle-build-action@842c587ad8aa4c68eeba24c396e15af4c2e9f30a # v2.9.0 + uses: gradle/gradle-build-action@87a9a15658c426a54dd469d4fc7dc1a73ca9d4a6 # v2.10.0 with: arguments: sdk:publish build-root-directory: languages/kotlin diff --git a/.github/workflows/build-java.yml b/.github/workflows/build-java.yml new file mode 100644 index 000000000..e500424dd --- /dev/null +++ b/.github/workflows/build-java.yml @@ -0,0 +1,68 @@ +name: Build Java SDK + +on: + pull_request: + branches: + - master + +jobs: + generate_schemas: + uses: ./.github/workflows/generate_schemas.yml + + build_rust: + uses: ./.github/workflows/build-rust-cross-platform.yml + + build_java: + name: Build Java + runs-on: ubuntu-22.04 + needs: + - generate_schemas + - build_rust + + steps: + - name: Checkout Repository + uses: actions/checkout@8ade135a41bc03ea155e62e844d188df1ea18608 # v4.1.0 + + - name: Download Java schemas artifact + uses: actions/download-artifact@9bc31d5ccc31df68ecc42ccf4149144866c47d8a # v3.0.2 + with: + name: sdk-schemas-java + path: languages/java/src/main/java/bit/sdk/schema/ + + - name: Setup Java + uses: actions/setup-java@0ab4596768b603586c0de567f2430c30f5b0d2b0 # v3.13.0 + with: + distribution: temurin + java-version: 17 + + - name: Download x86_64-apple-darwin files + uses: actions/download-artifact@9bc31d5ccc31df68ecc42ccf4149144866c47d8a # v3.0.2 + with: + name: libbitwarden_c_files-x86_64-apple-darwin + path: languages/java/src/main/resources/darwin-x64 + + - name: Download aarch64-apple-darwin files + uses: actions/download-artifact@9bc31d5ccc31df68ecc42ccf4149144866c47d8a # v3.0.2 + with: + name: libbitwarden_c_files-aarch64-apple-darwin + path: languages/java/src/main/resources/darwin-aarch64 + + - name: Download x86_64-unknown-linux-gnu files + uses: actions/download-artifact@9bc31d5ccc31df68ecc42ccf4149144866c47d8a # v3.0.2 + with: + name: libbitwarden_c_files-x86_64-unknown-linux-gnu + path: languages/java/src/main/resources/ubuntu-x64 + + - name: Download x86_64-pc-windows-msvc files + uses: actions/download-artifact@9bc31d5ccc31df68ecc42ccf4149144866c47d8a # v3.0.2 + with: + name: libbitwarden_c_files-x86_64-pc-windows-msvc + path: languages/java/src/main/resources/windows-x64 + + - name: Publish Maven + uses: gradle/gradle-build-action@b5126f31dbc19dd434c3269bf8c28c315e121da2 # v2.8.1 + with: + arguments: publish + build-root-directory: languages/java + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/build-napi.yml b/.github/workflows/build-napi.yml index 45fab6703..16f87a177 100644 --- a/.github/workflows/build-napi.yml +++ b/.github/workflows/build-napi.yml @@ -54,7 +54,7 @@ jobs: uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 - name: Setup Node - uses: actions/setup-node@5e21ff4d9bc1a8cf6de233a3057d20ec6b3fb69d # v3.8.1 + uses: actions/setup-node@8f152de45cc393bb48ce5d89d36b731f54556e65 # v4.0.0 with: node-version: 18 cache: "npm" diff --git a/.github/workflows/delete-old-packages.yml b/.github/workflows/delete-old-packages.yml index 517560ede..323a9a0a2 100644 --- a/.github/workflows/delete-old-packages.yml +++ b/.github/workflows/delete-old-packages.yml @@ -22,4 +22,4 @@ jobs: min-versions-to-keep: 25 # Ignore versions only containing version numbers - ignore-versions: '^\\d*\\.\\d*\\.\\d*$' + ignore-versions: '^\\d*\\.\\d*\\.\\d*(-SNAPSHOT)?$' diff --git a/.github/workflows/generate_schemas.yml b/.github/workflows/generate_schemas.yml index cac5bc2b8..b0517ea36 100644 --- a/.github/workflows/generate_schemas.yml +++ b/.github/workflows/generate_schemas.yml @@ -21,7 +21,7 @@ jobs: toolchain: stable - name: Set up Node - uses: actions/setup-node@5e21ff4d9bc1a8cf6de233a3057d20ec6b3fb69d # v3.8.1 + uses: actions/setup-node@8f152de45cc393bb48ce5d89d36b731f54556e65 # v4.0.0 with: cache: "npm" cache-dependency-path: "package-lock.json" @@ -57,9 +57,29 @@ jobs: path: ${{ github.workspace }}/languages/python/BitwardenClient/schemas.py if-no-files-found: error + - name: Upload ruby schemas artifact + uses: actions/upload-artifact@a8a3f3ad30e3422c9c7b888a15615d19a852ae32 # v3.1.3 + with: + name: schemas.rb + path: ${{ github.workspace }}/languages/ruby/bitwarden_sdk/lib/schemas.rb + if-no-files-found: error + - name: Upload json schemas artifact uses: actions/upload-artifact@a8a3f3ad30e3422c9c7b888a15615d19a852ae32 # v3.1.3 with: name: sdk-schemas-json path: ${{ github.workspace }}/support/schemas/* if-no-files-found: error + + - name: Upload Go schemas artifact + uses: actions/upload-artifact@0b7f8abb1508181956e8e162db84b466c27e18ce # v3.1.2 + with: + name: schemas.go + path: ${{ github.workspace }}/languages/go/schema.go + + - name: Upload java schemas artifact + uses: actions/upload-artifact@a8a3f3ad30e3422c9c7b888a15615d19a852ae32 # v3.1.3 + with: + name: sdk-schemas-java + path: ${{ github.workspace }}/languages/java/src/main/java/com/bitwarden/sdk/schema/* + if-no-files-found: error diff --git a/.github/workflows/golang-release.yml b/.github/workflows/golang-release.yml new file mode 100644 index 000000000..6f41bcd3e --- /dev/null +++ b/.github/workflows/golang-release.yml @@ -0,0 +1,73 @@ +name: Go Release + +on: + workflow_dispatch: + inputs: + version_number: + description: "New Version" + required: true + +env: + GO111MODULE: on + GO_VERSION: "^1.18" + +jobs: + build_rust: + uses: ./.github/workflows/build-rust-cross-platform.yml + + generate-schemas: + uses: ./.github/workflows/generate_schemas.yml + + build: + name: Build + needs: + - build_rust + - generate-schemas + runs-on: ubuntu-22.04 + steps: + - name: Checkout Repository + uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 + + - name: Setup Go environment + uses: actions/setup-go@93397bea11091df50f3d7e59dc26a7711a8bcfbe # v4.1.0 + with: + go-version: ${{ env.GO_VERSION }} + + - name: Cache dependencies + uses: actions/cache@704facf57e6136b1bc63b828d79edcd491f0ee84 # v3.3.2 + with: + path: ~/go/pkg/mod + key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }} + restore-keys: | + ${{ runner.os }}-go- + + - name: Build + run: go build -v ./... + + - name: Test + run: go test -v ./... + + release: + name: Release + needs: build + runs-on: ubuntu-22.04 + steps: + - name: Checkout Repository + uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 + + - name: Setup Go environment + uses: actions/setup-go@93397bea11091df50f3d7e59dc26a7711a8bcfbe # v4.1.0 + with: + go-version: ${{ env.GO_VERSION }} + + - name: Set release version + run: echo "VERSION=${{ github.event.inputs.version_number }}" >> $GITHUB_ENV + + - name: Install Goreleaser + run: go install github.com/goreleaser/goreleaser@v1.21.2 + + - name: Run Goreleaser + run: goreleaser release --rm-dist --skip-validate + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + VERSION: ${{ env.VERSION }} diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index a18e5064a..cb1fb2b67 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -31,7 +31,7 @@ jobs: run: cargo fmt --check - name: Set up Node - uses: actions/setup-node@5e21ff4d9bc1a8cf6de233a3057d20ec6b3fb69d # v3.8.1 + uses: actions/setup-node@8f152de45cc393bb48ce5d89d36b731f54556e65 # v4.0.0 with: cache: "npm" cache-dependency-path: "package-lock.json" diff --git a/.github/workflows/publish-ruby.yml b/.github/workflows/publish-ruby.yml new file mode 100644 index 000000000..da641dce5 --- /dev/null +++ b/.github/workflows/publish-ruby.yml @@ -0,0 +1,88 @@ +name: Publish Ruby SDK + +on: + pull_request: + branches: + - master + +jobs: + generate_schemas: + uses: ./.github/workflows/generate_schemas.yml + + build_rust: + uses: ./.github/workflows/build-rust-cross-platform.yml + + build_ruby: + name: Build Ruby + runs-on: ubuntu-22.04 + needs: + - generate_schemas + - build_rust + steps: + - name: Checkout Repository + uses: actions/checkout@c85c95e3d7251135ab7dc9ce3241c5835cc595a9 # v3.5.3 + + - name: Set up Ruby + uses: ruby/setup-ruby@54a18e26dbbb1eabc604f317ade9a5788dddef81 # v1.159.0 + with: + ruby-version: 3.2 + + - name: Download Ruby schemas artifact + uses: actions/download-artifact@9bc31d5ccc31df68ecc42ccf4149144866c47d8a # v3.0.2 + with: + name: schemas.rb + path: languages/ruby/bitwarden_sdk/lib + + - name: Download x86_64-apple-darwin files + uses: actions/download-artifact@9bc31d5ccc31df68ecc42ccf4149144866c47d8a # v3.0.2 + with: + name: libbitwarden_c_files-x86_64-apple-darwin + path: temp/macos-x64 + + - name: Download aarch64-apple-darwin files + uses: actions/download-artifact@9bc31d5ccc31df68ecc42ccf4149144866c47d8a # v3.0.2 + with: + name: libbitwarden_c_files-aarch64-apple-darwin + path: temp/macos-arm64 + + - name: Download x86_64-unknown-linux-gnu files + uses: actions/download-artifact@9bc31d5ccc31df68ecc42ccf4149144866c47d8a # v3.0.2 + with: + name: libbitwarden_c_files-x86_64-unknown-linux-gnu + path: temp/ubuntu-x64 + + - name: Download x86_64-pc-windows-msvc files + uses: actions/download-artifact@9bc31d5ccc31df68ecc42ccf4149144866c47d8a # v3.0.2 + with: + name: libbitwarden_c_files-x86_64-pc-windows-msvc + path: temp/windows-x64 + + - name: Copy lib files + run: | + mkdir -p languages/ruby/bitwarden_sdk/lib/macos-arm64 + mkdir -p languages/ruby/bitwarden_sdk/lib/ubuntu-x64 + mkdir -p languages/ruby/bitwarden_sdk/lib/macos-x64 + mkdir -p languages/ruby/bitwarden_sdk/lib/windows-x64 + + platforms=("macos-arm64" "ubuntu-x64" "macos-x64" "windows-x64") + files=("libbitwarden_c.dylib" "libbitwarden_c.so" "libbitwarden_c.dylib" "bitwarden_c.dll") + + for ((i=0; i<${#platforms[@]}; i++)); do + cp "temp/${platforms[$i]}/${files[$i]}" "languages/ruby/bitwarden_sdk/lib/${platforms[$i]}/${files[$i]}" + done + shell: bash + + - name: Build gem + run: gem build bitwarden-sdk.gemspec + working-directory: languages/ruby/bitwarden_sdk + + - name: Push gem to Rubygems + run: | + mkdir -p $HOME/.gem + touch $HOME/.gem/credentials + chmod 0600 $HOME/.gem/credentials + printf -- "---\n:rubygems_api_key: ${GEM_HOST_API_KEY}\n" > $HOME/.gem/credentials + gem push *.gem + env: + GEM_HOST_API_KEY: ${{ secrets.GEM_HOST_API_KEY }} + working-directory: languages/ruby/bitwarden_sdk diff --git a/.github/workflows/publish-rust-crates.yml b/.github/workflows/publish-rust-crates.yml index 1c5484a5c..b5a03ad47 100644 --- a/.github/workflows/publish-rust-crates.yml +++ b/.github/workflows/publish-rust-crates.yml @@ -103,13 +103,13 @@ jobs: uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 - name: Login to Azure - uses: Azure/login@92a5484dfaf04ca78a94597f4f19fea633851fa2 # v1.4.7 + uses: Azure/login@4c88f01b0e3a5600e08a37889921afd060f75cf0 # v1.5.0 with: creds: ${{ secrets.AZURE_KV_CI_SERVICE_PRINCIPAL }} - name: Retrieve secrets id: retrieve-secrets - uses: bitwarden/gh-actions/get-keyvault-secrets@c970b0fb89bd966749280e832928db62040812bf + uses: bitwarden/gh-actions/get-keyvault-secrets@main with: keyvault: "bitwarden-ci" secrets: "cratesio-api-token" diff --git a/.github/workflows/release-cli.yml b/.github/workflows/release-cli.yml index 3651e14bc..9ef782b7f 100644 --- a/.github/workflows/release-cli.yml +++ b/.github/workflows/release-cli.yml @@ -58,7 +58,7 @@ jobs: - name: Download all Release artifacts if: ${{ github.event.inputs.release_type != 'Dry Run' }} - uses: bitwarden/gh-actions/download-artifacts@c970b0fb89bd966749280e832928db62040812bf + uses: bitwarden/gh-actions/download-artifacts@main with: workflow: build-cli.yml path: packages @@ -67,7 +67,7 @@ jobs: - name: Dry Run - Download all artifacts if: ${{ github.event.inputs.release_type == 'Dry Run' }} - uses: bitwarden/gh-actions/download-artifacts@c970b0fb89bd966749280e832928db62040812bf + uses: bitwarden/gh-actions/download-artifacts@main with: workflow: build-cli.yml path: packages @@ -75,7 +75,7 @@ jobs: branch: master - name: Get checksum files - uses: bitwarden/gh-actions/get-checksum@c970b0fb89bd966749280e832928db62040812bf + uses: bitwarden/gh-actions/get-checksum@main with: packages_dir: "packages" file_path: "packages/bws-sha256-checksums-${{ steps.version.outputs.version }}.txt" @@ -128,13 +128,13 @@ jobs: uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 - name: Login to Azure - uses: Azure/login@92a5484dfaf04ca78a94597f4f19fea633851fa2 # v1.4.7 + uses: Azure/login@4c88f01b0e3a5600e08a37889921afd060f75cf0 # v1.5.0 with: creds: ${{ secrets.AZURE_KV_CI_SERVICE_PRINCIPAL }} - name: Retrieve secrets id: retrieve-secrets - uses: bitwarden/gh-actions/get-keyvault-secrets@c970b0fb89bd966749280e832928db62040812bf + uses: bitwarden/gh-actions/get-keyvault-secrets@main with: keyvault: "bitwarden-ci" secrets: "cratesio-api-token" diff --git a/.github/workflows/release-napi.yml b/.github/workflows/release-napi.yml index ee0407587..1e7b2e022 100644 --- a/.github/workflows/release-napi.yml +++ b/.github/workflows/release-napi.yml @@ -47,7 +47,7 @@ jobs: - name: Check Release Version id: version - uses: bitwarden/gh-actions/release-version-check@c970b0fb89bd966749280e832928db62040812bf + uses: bitwarden/gh-actions/release-version-check@main with: release-type: ${{ github.event.inputs.release_type }} project-type: ts @@ -93,7 +93,7 @@ jobs: uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 - name: Setup Node - uses: actions/setup-node@5e21ff4d9bc1a8cf6de233a3057d20ec6b3fb69d # v3.8.1 + uses: actions/setup-node@8f152de45cc393bb48ce5d89d36b731f54556e65 # v4.0.0 with: node-version: 18 cache: "npm" @@ -101,7 +101,7 @@ jobs: - name: Download schemas if: ${{ github.event.inputs.release_type != 'Dry Run' }} - uses: bitwarden/gh-actions/download-artifacts@c970b0fb89bd966749280e832928db62040812bf + uses: bitwarden/gh-actions/download-artifacts@main with: workflow: build-napi.yml artifacts: schemas.ts @@ -111,7 +111,7 @@ jobs: - name: Dry Run - Download schemas if: ${{ github.event.inputs.release_type == 'Dry Run' }} - uses: bitwarden/gh-actions/download-artifacts@c970b0fb89bd966749280e832928db62040812bf + uses: bitwarden/gh-actions/download-artifacts@main with: workflow: build-napi.yml artifacts: schemas.ts @@ -126,20 +126,20 @@ jobs: run: npm run tsc - name: Login to Azure - uses: Azure/login@92a5484dfaf04ca78a94597f4f19fea633851fa2 # v1.4.7 + uses: Azure/login@4c88f01b0e3a5600e08a37889921afd060f75cf0 # v1.5.0 with: creds: ${{ secrets.AZURE_KV_CI_SERVICE_PRINCIPAL }} - name: Retrieve secrets id: retrieve-secrets - uses: bitwarden/gh-actions/get-keyvault-secrets@c970b0fb89bd966749280e832928db62040812bf + uses: bitwarden/gh-actions/get-keyvault-secrets@main with: keyvault: "bitwarden-ci" secrets: "npm-api-key" - name: Download artifacts if: ${{ github.event.inputs.release_type != 'Dry Run' }} - uses: bitwarden/gh-actions/download-artifacts@c970b0fb89bd966749280e832928db62040812bf + uses: bitwarden/gh-actions/download-artifacts@main with: workflow: build-napi.yml path: ${{ github.workspace }}/crates/bitwarden-napi/artifacts @@ -148,7 +148,7 @@ jobs: - name: Dry Run - Download artifacts if: ${{ github.event.inputs.release_type == 'Dry Run' }} - uses: bitwarden/gh-actions/download-artifacts@c970b0fb89bd966749280e832928db62040812bf + uses: bitwarden/gh-actions/download-artifacts@main with: workflow: build-napi.yml path: ${{ github.workspace }}/crates/bitwarden-napi/artifacts diff --git a/.github/workflows/version-bump.yml b/.github/workflows/version-bump.yml index 29ee28f80..9fb441fa9 100644 --- a/.github/workflows/version-bump.yml +++ b/.github/workflows/version-bump.yml @@ -43,13 +43,13 @@ jobs: run: cargo install cargo-edit - name: Login to Azure - Prod Subscription - uses: Azure/login@92a5484dfaf04ca78a94597f4f19fea633851fa2 # v1.4.7 + uses: Azure/login@4c88f01b0e3a5600e08a37889921afd060f75cf0 # v1.5.0 with: creds: ${{ secrets.AZURE_KV_CI_SERVICE_PRINCIPAL }} - name: Retrieve secrets id: retrieve-secrets - uses: bitwarden/gh-actions/get-keyvault-secrets@c970b0fb89bd966749280e832928db62040812bf + uses: bitwarden/gh-actions/get-keyvault-secrets@main with: keyvault: "bitwarden-ci" secrets: "github-gpg-private-key, github-gpg-private-key-passphrase" diff --git a/.gitignore b/.gitignore index 3f459493b..007c3e839 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,7 @@ /target .DS_Store .pytest_cache +.vscode/c_cpp_properties.json # Build results [Dd]ebug/ @@ -49,3 +50,6 @@ crates/bitwarden-napi/src-ts/bitwarden_client/schemas.ts languages/csharp/Bitwarden.Sdk/schemas.cs languages/js_webassembly/bitwarden_client/schemas.ts languages/python/BitwardenClient/schemas.py +languages/cpp/include/schemas.hpp +languages/go/schema.go +languages/java/src/main/java/com/bitwarden/sdk/schema diff --git a/.vscode/settings.json b/.vscode/settings.json index e75498e9c..d49fe884f 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -15,6 +15,7 @@ "Pbkdf", "PKCS8", "repr", + "reqwest", "schemars", "uniffi", "wordlist" diff --git a/.vscode/tasks.json b/.vscode/tasks.json index 55dc10eb2..71400c06e 100644 --- a/.vscode/tasks.json +++ b/.vscode/tasks.json @@ -6,7 +6,7 @@ "command": "build", "problemMatcher": ["$rustc"], "options": { - "cwd": "${workspaceFolder}/bitwarden-c/" + "cwd": "${workspaceFolder}/crates/bitwarden-c/" }, "group": { "kind": "build", @@ -19,7 +19,7 @@ "command": "build", "args": ["--release"], "options": { - "cwd": "${workspaceFolder}/bitwarden-c/" + "cwd": "${workspaceFolder}/crates/bitwarden-c/" }, "problemMatcher": ["$rustc"], "label": "rust: bitwarden-c release build" @@ -46,6 +46,17 @@ "options": { "cwd": "${workspaceFolder}/languages/python" } + }, + { + "label": "buildJava", + "type": "shell", + "command": "gradle", + "args": ["build"], + "dependsOrder": "sequence", + "dependsOn": ["rust: bitwarden-c build"], + "options": { + "cwd": "${workspaceFolder}/languages/java" + } } ] } diff --git a/Cargo.lock b/Cargo.lock index 92cc6e5f4..50e263401 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -156,7 +156,7 @@ dependencies = [ "proc-macro2", "quote", "serde", - "syn 2.0.38", + "syn 2.0.39", ] [[package]] @@ -203,11 +203,11 @@ dependencies = [ [[package]] name = "async-lock" -version = "3.0.0" +version = "3.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "45e900cdcd39bb94a14487d3f7ef92ca222162e6c7c3fe7cb3550ea75fb486ed" +checksum = "dea8b3453dd7cc96711834b75400d671b73e3656975fa68d9f277163b7f7e316" dependencies = [ - "event-listener 3.0.1", + "event-listener 4.0.0", "event-listener-strategy", "pin-project-lite", ] @@ -220,7 +220,7 @@ checksum = "a66537f1bb974b254c98ed142ff995236e81b9d0fe4db0575f46612cb15eb0f9" dependencies = [ "proc-macro2", "quote", - "syn 2.0.38", + "syn 2.0.39", ] [[package]] @@ -287,7 +287,7 @@ dependencies = [ "flate2", "globset", "home", - "nu-ansi-term", + "nu-ansi-term 0.49.0", "once_cell", "path_abs", "plist", @@ -332,7 +332,7 @@ dependencies = [ "bitwarden-api-identity", "cbc", "chrono", - "getrandom 0.2.10", + "getrandom 0.2.11", "hkdf", "hmac", "lazy_static", @@ -341,6 +341,7 @@ dependencies = [ "num-traits", "pbkdf2", "rand 0.8.5", + "rand_chacha 0.3.1", "reqwest", "rsa", "schemars", @@ -350,6 +351,7 @@ dependencies = [ "serde_repr", "sha1", "sha2", + "subtle", "thiserror", "tokio", "uniffi", @@ -494,9 +496,9 @@ dependencies = [ [[package]] name = "bstr" -version = "1.7.0" +version = "1.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c79ad7fb2dd38f3dabd76b09c6a5a20c038fc0213ef1e9afd30eb777f120f019" +checksum = "542f33a8835a0884b006a0c3df3dadd99c0c3f296ed26c2fdc8028e01ad6230c" dependencies = [ "memchr", "serde", @@ -538,6 +540,7 @@ dependencies = [ "env_logger", "log", "openssl", + "regex", "serde", "serde_json", "serde_yaml", @@ -545,7 +548,7 @@ dependencies = [ "tempfile", "thiserror", "tokio", - "toml 0.8.6", + "toml 0.8.8", "uuid", ] @@ -584,9 +587,9 @@ dependencies = [ [[package]] name = "cargo-platform" -version = "0.1.4" +version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "12024c4645c97566567129c204f65d5815a8c9aecf30fcbe682b2fe034996d36" +checksum = "e34637b3140142bdf929fb439e8aa4ebad7651ebf7b1080b3930aa16ac1459ff" dependencies = [ "serde", ] @@ -654,9 +657,9 @@ dependencies = [ [[package]] name = "clap" -version = "4.4.7" +version = "4.4.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ac495e00dcec98c83465d5ad66c5c4fabd652fd6686e7c6269b117e729a6f17b" +checksum = "2275f18819641850fa26c89acc84d465c1bf91ce57bc2748b28c420473352f64" dependencies = [ "clap_builder", "clap_derive", @@ -664,9 +667,9 @@ dependencies = [ [[package]] name = "clap_builder" -version = "4.4.7" +version = "4.4.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c77ed9a32a62e6ca27175d00d29d05ca32e396ea1eb5fb01d8256b669cec7663" +checksum = "07cdf1b148b25c1e1f7a42225e30a0d99a615cd4637eae7365548dd4529b95bc" dependencies = [ "anstream", "anstyle", @@ -692,7 +695,7 @@ dependencies = [ "heck", "proc-macro2", "quote", - "syn 2.0.38", + "syn 2.0.39", ] [[package]] @@ -730,9 +733,9 @@ dependencies = [ [[package]] name = "color-spantrace" -version = "0.2.0" +version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1ba75b3d9449ecdccb27ecbc479fdc0b87fa2dd43d2f8298f9bf0e59aacc8dce" +checksum = "cd6be1b2a7e382e2b98b43b2adcca6bb0e465af0bdd38123873ae61eb17a72c2" dependencies = [ "once_cell", "owo-colors", @@ -923,7 +926,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "37e366bff8cd32dd8754b0991fb66b279dc48f598c3a18914852a6673deef583" dependencies = [ "quote", - "syn 2.0.38", + "syn 2.0.39", ] [[package]] @@ -1000,9 +1003,9 @@ dependencies = [ [[package]] name = "dyn-clone" -version = "1.0.14" +version = "1.0.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "23d2f3407d9a573d666de4b5bdf10569d73ca9478087346697dcbae6244bfbcd" +checksum = "545b22097d44f8a9581187cdf93de7a71e4722bf51200cfaba810865b49a495d" [[package]] name = "either" @@ -1027,9 +1030,9 @@ dependencies = [ [[package]] name = "env_logger" -version = "0.10.0" +version = "0.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "85cdab6a89accf66733ad5a1693a4dcced6aeff64602b634530dd73c1f3ee9f0" +checksum = "95b3f3e67048839cb0d0781f445682a35113da7121f7c949db0e2be96a4fbece" dependencies = [ "humantime", "is-terminal", @@ -1046,9 +1049,9 @@ checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" [[package]] name = "errno" -version = "0.3.5" +version = "0.3.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ac3e13f66a2f95e32a39eaa81f6b95d42878ca0e1db0c7543723dfe12557e860" +checksum = "f258a7194e7f7c2a7837a8913aeab7fd8c383457034fa20ce4dd3dcb813e8eb8" dependencies = [ "libc", "windows-sys 0.48.0", @@ -1062,9 +1065,9 @@ checksum = "0206175f82b8d6bf6652ff7d71a1e27fd2e4efde587fd368662814d6ec1d9ce0" [[package]] name = "event-listener" -version = "3.0.1" +version = "4.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "01cec0252c2afff729ee6f00e903d479fba81784c8e2bd77447673471fdfaea1" +checksum = "770d968249b5d99410d61f5bf89057f3199a077a04d087092f58e7d10692baae" dependencies = [ "concurrent-queue", "parking", @@ -1073,19 +1076,19 @@ dependencies = [ [[package]] name = "event-listener-strategy" -version = "0.3.0" +version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d96b852f1345da36d551b9473fa1e2b1eb5c5195585c6c018118bc92a8d91160" +checksum = "958e4d70b6d5e81971bebec42271ec641e7ff4e170a6fa605f2b8a8b65cb97d3" dependencies = [ - "event-listener 3.0.1", + "event-listener 4.0.0", "pin-project-lite", ] [[package]] name = "eyre" -version = "0.6.8" +version = "0.6.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4c2b6b5a29c02cdc822728b7d7b8ae1bab3e3b05d44522770ddd49722eeac7eb" +checksum = "80f656be11ddf91bd709454d15d5bd896fbaf4cc3314e69349e4d1569f5b46cd" dependencies = [ "indenter", "once_cell", @@ -1148,9 +1151,12 @@ dependencies = [ [[package]] name = "fs-err" -version = "2.9.0" +version = "2.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0845fa252299212f0389d64ba26f34fa32cfe41588355f21ed507c59a0f64541" +checksum = "fb5fd9bcbe8b1087cbd395b51498c01bc997cef73e778a80b77a811af5e2d29f" +dependencies = [ + "autocfg", +] [[package]] name = "futures" @@ -1223,7 +1229,7 @@ checksum = "53b153fd91e4b0147f4aced87be237c98248656bb01050b96bf3ee89220a8ddb" dependencies = [ "proc-macro2", "quote", - "syn 2.0.38", + "syn 2.0.39", ] [[package]] @@ -1262,6 +1268,19 @@ dependencies = [ "slab", ] +[[package]] +name = "generator" +version = "0.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5cc16584ff22b460a382b7feec54b23d2908d858152e5739a120b949293bd74e" +dependencies = [ + "cc", + "libc", + "log", + "rustversion", + "windows", +] + [[package]] name = "generic-array" version = "0.14.7" @@ -1285,9 +1304,9 @@ dependencies = [ [[package]] name = "getrandom" -version = "0.2.10" +version = "0.2.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "be4136b2a15dd319360be1c07d9933517ccf0be8f16bf62a3bee4f0d618df427" +checksum = "fe9006bed769170c11f845cf00c7c1e9092aeb3f268e007c3e760ac68008070f" dependencies = [ "cfg-if", "js-sys", @@ -1334,9 +1353,9 @@ dependencies = [ [[package]] name = "h2" -version = "0.3.21" +version = "0.3.22" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "91fc23aa11be92976ef4729127f1a74adf36d8436f7816b185d18df956790833" +checksum = "4d6250322ef6e60f93f9a2162799302cd6f68f79f6e5d85c8c16f14d1d958178" dependencies = [ "bytes", "fnv", @@ -1344,7 +1363,7 @@ dependencies = [ "futures-sink", "futures-util", "http", - "indexmap 1.9.3", + "indexmap 2.1.0", "slab", "tokio", "tokio-util", @@ -1404,9 +1423,9 @@ dependencies = [ [[package]] name = "http" -version = "0.2.9" +version = "0.2.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bd6effc99afb63425aff9b05836f029929e345a6148a14b7ecd5ab67af944482" +checksum = "8947b1a6fad4393052c7ba1f4cd97bed3e953a95c79c92ad9b051a04611d9fbb" dependencies = [ "bytes", "fnv", @@ -1552,9 +1571,9 @@ dependencies = [ [[package]] name = "indexmap" -version = "2.0.2" +version = "2.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8adf3ddd720272c6ea8bf59463c04e0f93d0bbf7c5439b691bca2987e0270897" +checksum = "d530e1a18b1cb4c484e6e34556a0d948706958449fca0cab753d649f2bce3d1f" dependencies = [ "equivalent", "hashbrown 0.14.2", @@ -1562,9 +1581,9 @@ dependencies = [ [[package]] name = "indoc" -version = "1.0.9" +version = "2.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bfa799dd5ed20a7e349f3b4639aa80d74549c81716d9ec4f994c9b5815598306" +checksum = "1e186cfbae8084e513daff4240b4797e342f988cecda4fb6c939150f96315fd8" [[package]] name = "infer" @@ -1632,9 +1651,9 @@ checksum = "616cde7c720bb2bb5824a224687d8f77bfd38922027f01d825cd7453be5099fb" [[package]] name = "itertools" -version = "0.11.0" +version = "0.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b1c173a5686ce8bfa551b3563d0c2170bf24ca44da99c7ca4bfdab5418c3fe57" +checksum = "25db6b064527c5d482d0423354fcd07a89a2dfe07b67892e62411946db7f07b0" dependencies = [ "either", ] @@ -1665,18 +1684,18 @@ dependencies = [ [[package]] name = "libc" -version = "0.2.149" +version = "0.2.150" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a08173bc88b7955d1b3145aa561539096c421ac8debde8cbc3612ec635fee29b" +checksum = "89d92a4743f9a61002fae18374ed11e7973f530cb3a3255fb354818118b2203c" [[package]] name = "libloading" -version = "0.7.4" +version = "0.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b67380fd3b2fbe7527a606e18729d21c6f3951633d0500574c4dc22d2d638b9f" +checksum = "c571b676ddfc9a8c12f1f3d3085a7b163966a8fd8098a90640953ce5f6170161" dependencies = [ "cfg-if", - "winapi", + "windows-sys 0.48.0", ] [[package]] @@ -1685,6 +1704,17 @@ version = "0.2.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4ec2a862134d2a7d32d7983ddcdd1c4923530833c9f2ea1a44fc5fa473989058" +[[package]] +name = "libredox" +version = "0.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85c833ca1e66078851dba29046874e38f08b2c883700aa29a03ddd3b23814ee8" +dependencies = [ + "bitflags 2.4.1", + "libc", + "redox_syscall", +] + [[package]] name = "line-wrap" version = "0.1.1" @@ -1696,9 +1726,9 @@ dependencies = [ [[package]] name = "linux-raw-sys" -version = "0.4.10" +version = "0.4.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "da2479e8c062e40bf0066ffa0bc823de0a9368974af99c9f6df941d2c231e03f" +checksum = "969488b55f8ac402214f3f5fd243ebb7206cf82de60d3172994707a4bcc2b829" [[package]] name = "lock_api" @@ -1716,6 +1746,29 @@ version = "0.4.20" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b5e6163cb8c49088c2c36f57875e58ccd8c87c7427f7fbd50ea6710b2f3f2e8f" +[[package]] +name = "loom" +version = "0.5.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff50ecb28bb86013e935fb6683ab1f6d3a20016f123c76fd4c27470076ac30f5" +dependencies = [ + "cfg-if", + "generator", + "pin-utils", + "scoped-tls", + "tracing", + "tracing-subscriber", +] + +[[package]] +name = "matchers" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8263075bb86c5a1b1427b5ae862e8889656f126e9f77c484496e8b47cf5c5558" +dependencies = [ + "regex-automata 0.1.10", +] + [[package]] name = "memchr" version = "2.6.4" @@ -1776,9 +1829,9 @@ dependencies = [ [[package]] name = "napi" -version = "2.13.3" +version = "2.14.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fd063c93b900149304e3ba96ce5bf210cd4f81ef5eb80ded0d100df3e85a3ac0" +checksum = "1133249c46e92da921bafc8aba4912bf84d6c475f7625183772ed2d0844dc3a7" dependencies = [ "bitflags 2.4.1", "ctor", @@ -1790,15 +1843,15 @@ dependencies = [ [[package]] name = "napi-build" -version = "2.0.1" +version = "2.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "882a73d9ef23e8dc2ebbffb6a6ae2ef467c0f18ac10711e4cc59c5485d41df0e" +checksum = "d4b4532cf86bfef556348ac65e561e3123879f0e7566cca6d43a6ff5326f13df" [[package]] name = "napi-derive" -version = "2.13.0" +version = "2.14.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "da1c6a8fa84d549aa8708fcd062372bf8ec6e849de39016ab921067d21bde367" +checksum = "a0cca5738c6e81eb5ffd2c8ff2b4f05ece9c4c60c7e2b36cec6524492cf7f330" dependencies = [ "cfg-if", "convert_case", @@ -1810,9 +1863,9 @@ dependencies = [ [[package]] name = "napi-derive-backend" -version = "1.0.52" +version = "1.0.55" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "20bbc7c69168d06a848f925ec5f0e0997f98e8c8d4f2cc30157f0da51c009e17" +checksum = "35960e5f33228192a9b661447d0dfe8f5a3790ff5b4058c4d67680ded4f65b91" dependencies = [ "convert_case", "once_cell", @@ -1825,9 +1878,9 @@ dependencies = [ [[package]] name = "napi-sys" -version = "2.2.3" +version = "2.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "166b5ef52a3ab5575047a9fe8d4a030cdd0f63c96f071cd6907674453b07bae3" +checksum = "2503fa6af34dc83fb74888df8b22afe933b58d37daf7d80424b1c60c68196b8b" dependencies = [ "libloading", ] @@ -1869,6 +1922,16 @@ dependencies = [ "minimal-lexical", ] +[[package]] +name = "nu-ansi-term" +version = "0.46.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77a8165726e8236064dbb45459242600304b42a5ea24ee2948e18e023bf7ba84" +dependencies = [ + "overload", + "winapi", +] + [[package]] name = "nu-ansi-term" version = "0.49.0" @@ -1962,6 +2025,15 @@ version = "1.18.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dd8b5dd2ae5ed71462c540258bedcb51965123ad7e7ccf4b9a8cafaa4a63576d" +[[package]] +name = "oneshot" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6f6640c6bda7731b1fdbab747981a0f896dd1fedaf9f4a53fa237a04a84431f4" +dependencies = [ + "loom", +] + [[package]] name = "onig" version = "6.4.0" @@ -1986,9 +2058,9 @@ dependencies = [ [[package]] name = "openssl" -version = "0.10.57" +version = "0.10.59" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bac25ee399abb46215765b1cb35bc0212377e58a061560d8b29b024fd0430e7c" +checksum = "7a257ad03cd8fb16ad4172fedf8094451e1af1c4b70097636ef2eac9a5f0cc33" dependencies = [ "bitflags 2.4.1", "cfg-if", @@ -2007,7 +2079,7 @@ checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c" dependencies = [ "proc-macro2", "quote", - "syn 2.0.38", + "syn 2.0.39", ] [[package]] @@ -2027,9 +2099,9 @@ dependencies = [ [[package]] name = "openssl-sys" -version = "0.9.93" +version = "0.9.95" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "db4d56a4c0478783083cfafcc42493dd4a981d41669da64b4572a2a089b51b1d" +checksum = "40a4130519a360279579c2053038317e40eff64d13fd3f004f9e1b72b8a6aaf9" dependencies = [ "cc", "libc", @@ -2044,6 +2116,12 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d" +[[package]] +name = "overload" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b15813163c1d831bf4a13c3610c05c0d03b39feb07f7e09fa234dac9b15aaf39" + [[package]] name = "owo-colors" version = "3.5.0" @@ -2074,7 +2152,7 @@ checksum = "4c42a9226546d68acdd9c0a280d17ce19bfe27a46bf68784e4066115788d008e" dependencies = [ "cfg-if", "libc", - "redox_syscall 0.4.1", + "redox_syscall", "smallvec", "windows-targets 0.48.5", ] @@ -2176,12 +2254,12 @@ checksum = "b4596b6d070b27117e987119b4dac604f3c58cfb0b191112e24771b2faeac1a6" [[package]] name = "plist" -version = "1.5.1" +version = "1.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9a4a0cfc5fb21a09dc6af4bf834cf10d4a32fccd9e2ea468c4b1751a097487aa" +checksum = "e5699cc8a63d1aa2b1ee8e12b9ad70ac790d65788cd36101fa37f87ea46c4cef" dependencies = [ "base64 0.21.5", - "indexmap 1.9.3", + "indexmap 2.1.0", "line-wrap", "quick-xml", "serde", @@ -2211,9 +2289,9 @@ dependencies = [ [[package]] name = "pyo3" -version = "0.19.2" +version = "0.20.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e681a6cfdc4adcc93b4d3cf993749a4552018ee0a9b65fc0ccfad74352c72a38" +checksum = "04e8453b658fe480c3e70c8ed4e3d3ec33eb74988bd186561b0cc66b85c3bc4b" dependencies = [ "cfg-if", "indoc", @@ -2228,9 +2306,9 @@ dependencies = [ [[package]] name = "pyo3-asyncio" -version = "0.19.0" +version = "0.20.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a2cc34c1f907ca090d7add03dc523acdd91f3a4dab12286604951e2f5152edad" +checksum = "6ea6b68e93db3622f3bb3bf363246cf948ed5375afe7abff98ccbdd50b184995" dependencies = [ "futures", "once_cell", @@ -2242,9 +2320,9 @@ dependencies = [ [[package]] name = "pyo3-asyncio-macros" -version = "0.19.0" +version = "0.20.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e4045f06429547179e4596f5c0b13c82efc8b04296016780133653ed69ce26b3" +checksum = "56c467178e1da6252c95c29ecf898b133f742e9181dca5def15dc24e19d45a39" dependencies = [ "proc-macro2", "quote", @@ -2253,9 +2331,9 @@ dependencies = [ [[package]] name = "pyo3-build-config" -version = "0.19.2" +version = "0.20.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "076c73d0bc438f7a4ef6fdd0c3bb4732149136abd952b110ac93e4edb13a6ba5" +checksum = "a96fe70b176a89cff78f2fa7b3c930081e163d5379b4dcdf993e3ae29ca662e5" dependencies = [ "once_cell", "target-lexicon", @@ -2263,9 +2341,9 @@ dependencies = [ [[package]] name = "pyo3-ffi" -version = "0.19.2" +version = "0.20.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e53cee42e77ebe256066ba8aa77eff722b3bb91f3419177cf4cd0f304d3284d9" +checksum = "214929900fd25e6604661ed9cf349727c8920d47deff196c4e28165a6ef2a96b" dependencies = [ "libc", "pyo3-build-config", @@ -2273,9 +2351,9 @@ dependencies = [ [[package]] name = "pyo3-log" -version = "0.8.4" +version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c09c2b349b6538d8a73d436ca606dab6ce0aaab4dad9e6b7bdd57a4f556c3bc3" +checksum = "4c10808ee7250403bedb24bc30c32493e93875fef7ba3e4292226fe924f398bd" dependencies = [ "arc-swap", "log", @@ -2284,32 +2362,33 @@ dependencies = [ [[package]] name = "pyo3-macros" -version = "0.19.2" +version = "0.20.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dfeb4c99597e136528c6dd7d5e3de5434d1ceaf487436a3f03b2d56b6fc9efd1" +checksum = "dac53072f717aa1bfa4db832b39de8c875b7c7af4f4a6fe93cdbf9264cf8383b" dependencies = [ "proc-macro2", "pyo3-macros-backend", "quote", - "syn 1.0.109", + "syn 2.0.39", ] [[package]] name = "pyo3-macros-backend" -version = "0.19.2" +version = "0.20.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "947dc12175c254889edc0c02e399476c2f652b4b9ebd123aa655c224de259536" +checksum = "7774b5a8282bd4f25f803b1f0d945120be959a36c72e08e7cd031c792fdfd424" dependencies = [ + "heck", "proc-macro2", "quote", - "syn 1.0.109", + "syn 2.0.39", ] [[package]] name = "quick-xml" -version = "0.30.0" +version = "0.31.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eff6510e86862b57b210fd8cbe8ed3f0d7d600b9c2863cd4549a2e033c66e956" +checksum = "1004a344b30a54e2ee58d66a71b32d2db2feb0a31f9a2d302bf0536f15de2a33" dependencies = [ "memchr", ] @@ -2382,7 +2461,7 @@ version = "0.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" dependencies = [ - "getrandom 0.2.10", + "getrandom 0.2.11", ] [[package]] @@ -2394,15 +2473,6 @@ dependencies = [ "rand_core 0.5.1", ] -[[package]] -name = "redox_syscall" -version = "0.2.16" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fb5a58c1855b4b6819d59012155603f0b22ad30cad752600aadfcb695265519a" -dependencies = [ - "bitflags 1.3.2", -] - [[package]] name = "redox_syscall" version = "0.4.1" @@ -2414,12 +2484,12 @@ dependencies = [ [[package]] name = "redox_users" -version = "0.4.3" +version = "0.4.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b033d837a7cf162d7993aded9304e30a83213c648b6e389db233191f891e5c2b" +checksum = "a18479200779601e498ada4e8c1e1f50e3ee19deb0259c25825a98b5603b2cb4" dependencies = [ - "getrandom 0.2.10", - "redox_syscall 0.2.16", + "getrandom 0.2.11", + "libredox", "thiserror", ] @@ -2431,10 +2501,19 @@ checksum = "380b951a9c5e80ddfd6136919eef32310721aa4aacd4889a8d39124b026ab343" dependencies = [ "aho-corasick", "memchr", - "regex-automata", + "regex-automata 0.4.3", "regex-syntax 0.8.2", ] +[[package]] +name = "regex-automata" +version = "0.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c230d73fb8d8c1b9c0b3135c5142a8acee3a0558fb8db5cf1cb65f8d7862132" +dependencies = [ + "regex-syntax 0.6.29", +] + [[package]] name = "regex-automata" version = "0.4.3" @@ -2446,6 +2525,12 @@ dependencies = [ "regex-syntax 0.8.2", ] +[[package]] +name = "regex-syntax" +version = "0.6.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f162c6dd7b008981e4d40210aca20b4bd0f9b60ca9271061b07f78537722f2e1" + [[package]] name = "regex-syntax" version = "0.7.5" @@ -2540,9 +2625,9 @@ checksum = "d626bb9dae77e28219937af045c257c28bfd3f69333c512553507f5f9798cb76" [[package]] name = "rustix" -version = "0.38.21" +version = "0.38.25" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2b426b0506e5d50a7d8dafcf2e81471400deb602392c7dd110815afb4eaf02a3" +checksum = "dc99bc2d4f1fed22595588a013687477aedf3cdcfb26558c559edb67b4d9b22e" dependencies = [ "bitflags 2.4.1", "errno", @@ -2589,9 +2674,9 @@ dependencies = [ [[package]] name = "schemars" -version = "0.8.15" +version = "0.8.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1f7b0ce13155372a76ee2e1c5ffba1fe61ede73fbea5630d61eee6fac4929c0c" +checksum = "45a28f4c49489add4ce10783f7911893516f15afe45d015608d41faca6bc4d29" dependencies = [ "chrono", "dyn-clone", @@ -2604,9 +2689,9 @@ dependencies = [ [[package]] name = "schemars_derive" -version = "0.8.15" +version = "0.8.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e85e2a16b12bdb763244c69ab79363d71db2b4b918a2def53f80b02e0574b13c" +checksum = "c767fd6fa65d9ccf9cf026122c1b555f2ef9a4f0cea69da4d7dbc3e258d30967" dependencies = [ "proc-macro2", "quote", @@ -2643,7 +2728,7 @@ checksum = "1db149f81d46d2deba7cd3c50772474707729550221e69588478ebf9ada425ae" dependencies = [ "proc-macro2", "quote", - "syn 2.0.38", + "syn 2.0.39", ] [[package]] @@ -2693,22 +2778,22 @@ dependencies = [ [[package]] name = "serde" -version = "1.0.190" +version = "1.0.193" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "91d3c334ca1ee894a2c6f6ad698fe8c435b76d504b13d436f0685d648d6d96f7" +checksum = "25dd9975e68d0cb5aa1120c288333fc98731bd1dd12f561e468ea4728c042b89" dependencies = [ "serde_derive", ] [[package]] name = "serde_derive" -version = "1.0.190" +version = "1.0.193" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "67c5609f394e5c2bd7fc51efda478004ea80ef42fee983d5c67a65e34f32c0e3" +checksum = "43576ca501357b9b071ac53cdc7da8ef0cbd9493d8df094cd821777ea6e894d3" dependencies = [ "proc-macro2", "quote", - "syn 2.0.38", + "syn 2.0.39", ] [[package]] @@ -2763,7 +2848,7 @@ checksum = "3081f5ffbb02284dda55132aa26daecedd7372a42417bbbab6f14ab7d6bb9145" dependencies = [ "proc-macro2", "quote", - "syn 2.0.38", + "syn 2.0.39", ] [[package]] @@ -2793,7 +2878,7 @@ version = "0.9.27" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3cc7a1570e38322cfe4154732e5110f887ea57e22b76f4bfd32b5bdd3368666c" dependencies = [ - "indexmap 2.0.2", + "indexmap 2.1.0", "itoa", "ryu", "serde", @@ -2863,9 +2948,9 @@ dependencies = [ [[package]] name = "signature" -version = "2.1.0" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5e1788eed21689f9cf370582dfc467ef36ed9c707f073528ddafa8d83e3b8500" +checksum = "77549399552de45a898a580c1b41d445bf730df867cc44e6c0233bbc4b8329de" dependencies = [ "digest", "rand_core 0.6.4", @@ -2888,9 +2973,15 @@ dependencies = [ [[package]] name = "smallvec" -version = "1.11.1" +version = "1.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4dccd0940a2dcdf68d092b8cbab7dc0ad8fa938bf95787e1b916b0e3d0e8e970" + +[[package]] +name = "smawk" +version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "942b4a808e05215192e39f4ab80813e599068285906cc91aa64f923db842bd5a" +checksum = "b7c388c1b5e93756d0c740965c41e8822f866621d41acbdf6336a6a168f8840c" [[package]] name = "socket2" @@ -2962,7 +3053,7 @@ dependencies = [ "proc-macro2", "quote", "rustversion", - "syn 2.0.38", + "syn 2.0.39", ] [[package]] @@ -2994,9 +3085,9 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.38" +version = "2.0.39" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e96b79aaa137db8f61e26363a0c9b47d8b4ec75da28b7d1d614c2303e232408b" +checksum = "23e78b90f2fcf45d3e842032ce32e3f2d1545ba6636271dcbf24fa306d87be7a" dependencies = [ "proc-macro2", "quote", @@ -3057,20 +3148,31 @@ checksum = "7ef1adac450ad7f4b3c28589471ade84f25f731a7a0fe30d71dfa9f60fd808e5" dependencies = [ "cfg-if", "fastrand 2.0.1", - "redox_syscall 0.4.1", + "redox_syscall", "rustix", "windows-sys 0.48.0", ] [[package]] name = "termcolor" -version = "1.3.0" +version = "1.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6093bad37da69aab9d123a8091e4be0aa4a03e4d601ec641c327398315f62b64" +checksum = "ff1bc3d3f05aff0403e8ac0d92ced918ec05b666a43f83297ccef5bea8a3d449" dependencies = [ "winapi-util", ] +[[package]] +name = "textwrap" +version = "0.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "222a222a5bfe1bba4a77b45ec488a741b3cb8872e5e499451fd7d0129c9c7c3d" +dependencies = [ + "smawk", + "unicode-linebreak", + "unicode-width", +] + [[package]] name = "thiserror" version = "1.0.50" @@ -3088,7 +3190,7 @@ checksum = "266b2e40bc00e5a6c09c3584011e08b06f123c00362c92b975ba9843aaaa14b8" dependencies = [ "proc-macro2", "quote", - "syn 2.0.38", + "syn 2.0.39", ] [[package]] @@ -3147,9 +3249,9 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" [[package]] name = "tokio" -version = "1.33.0" +version = "1.34.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4f38200e3ef7995e5ef13baec2f432a6da0aa9ac495b2c0e8f3b7eec2c92d653" +checksum = "d0c014766411e834f7af5b8f4cf46257aab4036ca95e9d2c144a10f59ad6f5b9" dependencies = [ "backtrace", "bytes", @@ -3164,13 +3266,13 @@ dependencies = [ [[package]] name = "tokio-macros" -version = "2.1.0" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "630bdcf245f78637c13ec01ffae6187cca34625e8c63150d424b59e55af2675e" +checksum = "5b8a1e28f2deaa14e508979454cb3a223b10b938b45af148bc0986de36f1923b" dependencies = [ "proc-macro2", "quote", - "syn 2.0.38", + "syn 2.0.39", ] [[package]] @@ -3208,9 +3310,9 @@ dependencies = [ [[package]] name = "toml" -version = "0.8.6" +version = "0.8.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8ff9e3abce27ee2c9a37f9ad37238c1bdd4e789c84ba37df76aa4d528f5072cc" +checksum = "a1a195ec8c9da26928f773888e0742ca3ca1040c6cd859c919c9f59c1954ab35" dependencies = [ "serde", "serde_spanned", @@ -3229,11 +3331,11 @@ dependencies = [ [[package]] name = "toml_edit" -version = "0.20.7" +version = "0.21.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "70f427fce4d84c72b5b732388bf4a9f4531b53f74e2887e3ecb2481f68f66d81" +checksum = "d34d383cd00a163b4a5b85053df514d45bc330f6de7737edfe0a93311d1eaa03" dependencies = [ - "indexmap 2.0.2", + "indexmap 2.1.0", "serde", "serde_spanned", "toml_datetime", @@ -3253,9 +3355,21 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c3523ab5a71916ccf420eebdf5521fcef02141234bbc0b8a49f2fdc4544364ef" dependencies = [ "pin-project-lite", + "tracing-attributes", "tracing-core", ] +[[package]] +name = "tracing-attributes" +version = "0.1.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34704c8d6ebcbc939824180af020566b01a7c01f80641264eba0999f6c2b6be7" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.39", +] + [[package]] name = "tracing-core" version = "0.1.32" @@ -3276,15 +3390,33 @@ dependencies = [ "tracing-subscriber", ] +[[package]] +name = "tracing-log" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee855f1f400bd0e5c02d150ae5de3840039a3f54b025156404e34c23c03f47c3" +dependencies = [ + "log", + "once_cell", + "tracing-core", +] + [[package]] name = "tracing-subscriber" -version = "0.3.17" +version = "0.3.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "30a651bc37f915e81f087d86e62a18eec5f79550c7faff886f7090b4ea757c77" +checksum = "ad0f048c97dbd9faa9b7df56362b8ebcaa52adb06b498c050d2f4e32f90a7a8b" dependencies = [ + "matchers", + "nu-ansi-term 0.46.0", + "once_cell", + "regex", "sharded-slab", + "smallvec", "thread_local", + "tracing", "tracing-core", + "tracing-log", ] [[package]] @@ -3320,6 +3452,12 @@ version = "1.0.12" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b" +[[package]] +name = "unicode-linebreak" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b09c83c3c29d37506a3e260c08c03743a6bb66a9cd432c6934ab501a190571f" + [[package]] name = "unicode-normalization" version = "0.1.22" @@ -3343,8 +3481,8 @@ checksum = "e51733f11c9c4f72aa0c160008246859e340b00807569a0da0e7a1079b27ba85" [[package]] name = "uniffi" -version = "0.24.1" -source = "git+https://github.com/mozilla/uniffi-rs?rev=53d5ac7274d8b4d66ad35b68cb6e2d89898f96af#53d5ac7274d8b4d66ad35b68cb6e2d89898f96af" +version = "0.25.2" +source = "git+https://github.com/mozilla/uniffi-rs?rev=23711c8151bbb794369aa1f9d383db386792dff9#23711c8151bbb794369aa1f9d383db386792dff9" dependencies = [ "anyhow", "camino", @@ -3364,8 +3502,8 @@ dependencies = [ [[package]] name = "uniffi_bindgen" -version = "0.24.1" -source = "git+https://github.com/mozilla/uniffi-rs?rev=53d5ac7274d8b4d66ad35b68cb6e2d89898f96af#53d5ac7274d8b4d66ad35b68cb6e2d89898f96af" +version = "0.25.2" +source = "git+https://github.com/mozilla/uniffi-rs?rev=23711c8151bbb794369aa1f9d383db386792dff9#23711c8151bbb794369aa1f9d383db386792dff9" dependencies = [ "anyhow", "askama", @@ -3379,7 +3517,7 @@ dependencies = [ "once_cell", "paste", "serde", - "serde_json", + "textwrap", "toml 0.5.11", "uniffi_meta", "uniffi_testing", @@ -3388,8 +3526,8 @@ dependencies = [ [[package]] name = "uniffi_build" -version = "0.24.1" -source = "git+https://github.com/mozilla/uniffi-rs?rev=53d5ac7274d8b4d66ad35b68cb6e2d89898f96af#53d5ac7274d8b4d66ad35b68cb6e2d89898f96af" +version = "0.25.2" +source = "git+https://github.com/mozilla/uniffi-rs?rev=23711c8151bbb794369aa1f9d383db386792dff9#23711c8151bbb794369aa1f9d383db386792dff9" dependencies = [ "anyhow", "camino", @@ -3398,32 +3536,32 @@ dependencies = [ [[package]] name = "uniffi_checksum_derive" -version = "0.24.1" -source = "git+https://github.com/mozilla/uniffi-rs?rev=53d5ac7274d8b4d66ad35b68cb6e2d89898f96af#53d5ac7274d8b4d66ad35b68cb6e2d89898f96af" +version = "0.25.2" +source = "git+https://github.com/mozilla/uniffi-rs?rev=23711c8151bbb794369aa1f9d383db386792dff9#23711c8151bbb794369aa1f9d383db386792dff9" dependencies = [ "quote", - "syn 2.0.38", + "syn 2.0.39", ] [[package]] name = "uniffi_core" -version = "0.24.1" -source = "git+https://github.com/mozilla/uniffi-rs?rev=53d5ac7274d8b4d66ad35b68cb6e2d89898f96af#53d5ac7274d8b4d66ad35b68cb6e2d89898f96af" +version = "0.25.2" +source = "git+https://github.com/mozilla/uniffi-rs?rev=23711c8151bbb794369aa1f9d383db386792dff9#23711c8151bbb794369aa1f9d383db386792dff9" dependencies = [ "anyhow", "bytes", "camino", - "cargo_metadata", "log", "once_cell", + "oneshot", "paste", "static_assertions", ] [[package]] name = "uniffi_macros" -version = "0.24.1" -source = "git+https://github.com/mozilla/uniffi-rs?rev=53d5ac7274d8b4d66ad35b68cb6e2d89898f96af#53d5ac7274d8b4d66ad35b68cb6e2d89898f96af" +version = "0.25.2" +source = "git+https://github.com/mozilla/uniffi-rs?rev=23711c8151bbb794369aa1f9d383db386792dff9#23711c8151bbb794369aa1f9d383db386792dff9" dependencies = [ "bincode", "camino", @@ -3432,7 +3570,7 @@ dependencies = [ "proc-macro2", "quote", "serde", - "syn 2.0.38", + "syn 2.0.39", "toml 0.5.11", "uniffi_build", "uniffi_meta", @@ -3440,36 +3578,34 @@ dependencies = [ [[package]] name = "uniffi_meta" -version = "0.24.1" -source = "git+https://github.com/mozilla/uniffi-rs?rev=53d5ac7274d8b4d66ad35b68cb6e2d89898f96af#53d5ac7274d8b4d66ad35b68cb6e2d89898f96af" +version = "0.25.2" +source = "git+https://github.com/mozilla/uniffi-rs?rev=23711c8151bbb794369aa1f9d383db386792dff9#23711c8151bbb794369aa1f9d383db386792dff9" dependencies = [ "anyhow", "bytes", - "serde", "siphasher", "uniffi_checksum_derive", ] [[package]] name = "uniffi_testing" -version = "0.24.1" -source = "git+https://github.com/mozilla/uniffi-rs?rev=53d5ac7274d8b4d66ad35b68cb6e2d89898f96af#53d5ac7274d8b4d66ad35b68cb6e2d89898f96af" +version = "0.25.2" +source = "git+https://github.com/mozilla/uniffi-rs?rev=23711c8151bbb794369aa1f9d383db386792dff9#23711c8151bbb794369aa1f9d383db386792dff9" dependencies = [ "anyhow", "camino", "cargo_metadata", "fs-err", "once_cell", - "serde", - "serde_json", ] [[package]] name = "uniffi_udl" -version = "0.24.1" -source = "git+https://github.com/mozilla/uniffi-rs?rev=53d5ac7274d8b4d66ad35b68cb6e2d89898f96af#53d5ac7274d8b4d66ad35b68cb6e2d89898f96af" +version = "0.25.2" +source = "git+https://github.com/mozilla/uniffi-rs?rev=23711c8151bbb794369aa1f9d383db386792dff9#23711c8151bbb794369aa1f9d383db386792dff9" dependencies = [ "anyhow", + "textwrap", "uniffi_meta", "uniffi_testing", "weedle2", @@ -3477,9 +3613,9 @@ dependencies = [ [[package]] name = "unindent" -version = "0.1.11" +version = "0.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e1766d682d402817b5ac4490b3c3002d91dfa0d22812f341609f97b08757359c" +checksum = "c7de7d73e1754487cb58364ee906a499937a0dfabd86bcb980fa99ec8c8fa2ce" [[package]] name = "unsafe-libyaml" @@ -3507,9 +3643,9 @@ checksum = "711b9620af191e0cdc7468a8d14e709c3dcdb115b36f838e601583af800a370a" [[package]] name = "uuid" -version = "1.5.0" +version = "1.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "88ad59a7560b41a70d191093a945f0b87bc1deeda46fb237479708a1d6b6cdfc" +checksum = "5e395fcf16a7a3d8127ec99782007af141946b4795001f876d54fb0d55978560" dependencies = [ "serde", ] @@ -3592,7 +3728,7 @@ dependencies = [ "once_cell", "proc-macro2", "quote", - "syn 2.0.38", + "syn 2.0.39", "wasm-bindgen-shared", ] @@ -3626,7 +3762,7 @@ checksum = "54681b18a46765f095758388f2d0cf16eb8d4169b639ab575a8f5693af210c7b" dependencies = [ "proc-macro2", "quote", - "syn 2.0.38", + "syn 2.0.39", "wasm-bindgen-backend", "wasm-bindgen-shared", ] @@ -3674,7 +3810,7 @@ dependencies = [ [[package]] name = "weedle2" version = "4.0.0" -source = "git+https://github.com/mozilla/uniffi-rs?rev=53d5ac7274d8b4d66ad35b68cb6e2d89898f96af#53d5ac7274d8b4d66ad35b68cb6e2d89898f96af" +source = "git+https://github.com/mozilla/uniffi-rs?rev=23711c8151bbb794369aa1f9d383db386792dff9#23711c8151bbb794369aa1f9d383db386792dff9" dependencies = [ "nom", ] @@ -3710,6 +3846,15 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" +[[package]] +name = "windows" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e686886bc078bc1b0b600cac0147aadb815089b6e4da64016cbd754b6342700f" +dependencies = [ + "windows-targets 0.48.5", +] + [[package]] name = "windows-core" version = "0.51.1" @@ -3853,9 +3998,9 @@ checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" [[package]] name = "winnow" -version = "0.5.17" +version = "0.5.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a3b801d0e0a6726477cc207f60162da452f3a95adb368399bef20a946e06f65c" +checksum = "829846f3e3db426d4cee4510841b71a8e58aa2a76b1132579487ae430ccd9c7b" dependencies = [ "memchr", ] @@ -3872,9 +4017,9 @@ dependencies = [ [[package]] name = "wiremock" -version = "0.5.19" +version = "0.5.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c6f71803d3a1c80377a06221e0530be02035d5b3e854af56c6ece7ac20ac441d" +checksum = "079aee011e8a8e625d16df9e785de30a6b77f80a6126092d76a57375f96448da" dependencies = [ "assert-json-diff", "async-trait", @@ -3894,6 +4039,6 @@ dependencies = [ [[package]] name = "zeroize" -version = "1.6.0" +version = "1.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2a0956f1ba7c7909bfb66c2e9e4124ab6f6482560f6628b5aaeba39207c9aad9" +checksum = "525b4ec142c6b68a2d10f01f7bbf6755599ca3f81ea53b8431b7dd348f5fdb2d" diff --git a/Cargo.toml b/Cargo.toml index dbd4ed30f..f841f07c9 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -22,10 +22,10 @@ codegen-units = 1 # This is fine as long as we don't have any unhandled panics, but let's keep it disabled for now # strip = true -# Uniffi proc-macro support is still not part of a release, so we need to use the git version for now +# Using git dependency temporarily to add support for immutable records in generated code [patch.crates-io] -uniffi = { git = "https://github.com/mozilla/uniffi-rs", rev = "53d5ac7274d8b4d66ad35b68cb6e2d89898f96af" } -uniffi_build = { git = "https://github.com/mozilla/uniffi-rs", rev = "53d5ac7274d8b4d66ad35b68cb6e2d89898f96af" } -uniffi_bindgen = { git = "https://github.com/mozilla/uniffi-rs", rev = "53d5ac7274d8b4d66ad35b68cb6e2d89898f96af" } -uniffi_core = { git = "https://github.com/mozilla/uniffi-rs", rev = "53d5ac7274d8b4d66ad35b68cb6e2d89898f96af" } -uniffi_macros = { git = "https://github.com/mozilla/uniffi-rs", rev = "53d5ac7274d8b4d66ad35b68cb6e2d89898f96af" } +uniffi = { git = "https://github.com/mozilla/uniffi-rs", rev = "23711c8151bbb794369aa1f9d383db386792dff9" } +uniffi_build = { git = "https://github.com/mozilla/uniffi-rs", rev = "23711c8151bbb794369aa1f9d383db386792dff9" } +uniffi_bindgen = { git = "https://github.com/mozilla/uniffi-rs", rev = "23711c8151bbb794369aa1f9d383db386792dff9" } +uniffi_core = { git = "https://github.com/mozilla/uniffi-rs", rev = "23711c8151bbb794369aa1f9d383db386792dff9" } +uniffi_macros = { git = "https://github.com/mozilla/uniffi-rs", rev = "23711c8151bbb794369aa1f9d383db386792dff9" } diff --git a/README.md b/README.md index 18613d4dc..2de94b09c 100644 --- a/README.md +++ b/README.md @@ -57,10 +57,10 @@ The first step is to generate the swagger documents from the server repository. ```bash # src/Api -dotnet swagger tofile --output ../../api.json .\bin\Debug\net6.0\Api.dll internal +dotnet swagger tofile --output ../../api.json ./bin/Debug/net6.0/Api.dll internal # src/Identity -dotnet swagger tofile --output ../../identity.json .\bin\Debug\net6.0\Identity.dll v1 +ASPNETCORE_ENVIRONMENT=development dotnet swagger tofile --output ../../identity.json ./bin/Debug/net6.0/Identity.dll v1 ``` ### OpenApi Generator @@ -68,20 +68,20 @@ dotnet swagger tofile --output ../../identity.json .\bin\Debug\net6.0\Identity.d Runs from the root of the SDK project. ```bash -npx openapi-generator-cli generate ` - -i ../server/api.json ` - -g rust ` - -o crates/bitwarden-api-api ` - --package-name bitwarden-api-api ` - -t ./support/openapi-template ` +npx openapi-generator-cli generate \ + -i ../server/api.json \ + -g rust \ + -o crates/bitwarden-api-api \ + --package-name bitwarden-api-api \ + -t ./support/openapi-template \ --additional-properties=packageVersion=1.0.0 -npx openapi-generator-cli generate ` - -i ../server/identity.json ` - -g rust ` - -o crates/bitwarden-api-identity ` - --package-name bitwarden-api-identity ` - -t ./support/openapi-template ` +npx openapi-generator-cli generate \ + -i ../server/identity.json \ + -g rust \ + -o crates/bitwarden-api-identity \ + --package-name bitwarden-api-identity \ + -t ./support/openapi-template \ --additional-properties=packageVersion=1.0.0 ``` diff --git a/crates/bitwarden-api-api/.openapi-generator/FILES b/crates/bitwarden-api-api/.openapi-generator/FILES index 4fbc2fbf9..586d4f2db 100644 --- a/crates/bitwarden-api-api/.openapi-generator/FILES +++ b/crates/bitwarden-api-api/.openapi-generator/FILES @@ -69,6 +69,7 @@ src/models/authenticator_attestation_raw_response.rs src/models/base_access_policy_response_model.rs src/models/base_secret_response_model.rs src/models/base_secret_response_model_list_response_model.rs +src/models/billing_customer_discount.rs src/models/billing_history_response_model.rs src/models/billing_invoice.rs src/models/billing_payment_response_model.rs @@ -122,6 +123,7 @@ src/models/collection_with_id_request_model.rs src/models/config_response_model.rs src/models/delete_recover_request_model.rs src/models/device_keys_request_model.rs +src/models/device_keys_update_request_model.rs src/models/device_request_model.rs src/models/device_response_model.rs src/models/device_response_model_list_response_model.rs @@ -201,10 +203,10 @@ src/models/organization_domain_response_model.rs src/models/organization_domain_response_model_list_response_model.rs src/models/organization_domain_sso_details_request_model.rs src/models/organization_domain_sso_details_response_model.rs -src/models/organization_enroll_secrets_manager_request_model.rs src/models/organization_keys_request_model.rs src/models/organization_keys_response_model.rs src/models/organization_license.rs +src/models/organization_public_key_response_model.rs src/models/organization_response_model.rs src/models/organization_seat_request_model.rs src/models/organization_sponsorship_create_request_model.rs @@ -241,6 +243,7 @@ src/models/organization_user_update_request_model.rs src/models/organization_user_user_details_response_model.rs src/models/organization_user_user_details_response_model_list_response_model.rs src/models/organization_verify_bank_request_model.rs +src/models/other_device_keys_update_request_model.rs src/models/password_hint_request_model.rs src/models/password_request_model.rs src/models/payment_method_type.rs @@ -272,6 +275,7 @@ src/models/project_create_request_model.rs src/models/project_response_model.rs src/models/project_response_model_list_response_model.rs src/models/project_update_request_model.rs +src/models/protected_device_response_model.rs src/models/provider_organization_add_request_model.rs src/models/provider_organization_create_request_model.rs src/models/provider_organization_organization_details_response_model.rs @@ -316,6 +320,7 @@ src/models/secret_update_request_model.rs src/models/secret_verification_request_model.rs src/models/secret_with_projects_inner_project.rs src/models/secret_with_projects_list_response_model.rs +src/models/secrets_manager_subscribe_request_model.rs src/models/secrets_manager_subscription_update_request_model.rs src/models/secrets_with_projects_inner_secret.rs src/models/secure_note_type.rs @@ -337,7 +342,8 @@ src/models/service_account_create_request_model.rs src/models/service_account_project_access_policy_response_model.rs src/models/service_account_project_access_policy_response_model_list_response_model.rs src/models/service_account_response_model.rs -src/models/service_account_response_model_list_response_model.rs +src/models/service_account_secrets_details_response_model.rs +src/models/service_account_secrets_details_response_model_list_response_model.rs src/models/service_account_update_request_model.rs src/models/set_key_connector_key_request_model.rs src/models/set_password_request_model.rs @@ -370,6 +376,7 @@ src/models/two_factor_web_authn_request_model.rs src/models/two_factor_web_authn_response_model.rs src/models/two_factor_yubi_key_response_model.rs src/models/update_avatar_request_model.rs +src/models/update_devices_trust_request_model.rs src/models/update_domains_request_model.rs src/models/update_key_request_model.rs src/models/update_profile_request_model.rs diff --git a/crates/bitwarden-api-api/Cargo.toml b/crates/bitwarden-api-api/Cargo.toml index 518cc33ed..2e05088e0 100644 --- a/crates/bitwarden-api-api/Cargo.toml +++ b/crates/bitwarden-api-api/Cargo.toml @@ -13,14 +13,14 @@ categories = ["api-bindings"] edition = "2018" [dependencies] -serde = "^1.0.163" -serde_derive = "^1.0.163" -serde_json = "^1.0.96" -serde_repr = "^0.1.12" -url = "^2.3.1" -uuid = { version = "^1.3.3", features = ["serde"] } +serde = ">=1.0.163, <2" +serde_derive = ">=1.0.163, <2" +serde_json = ">=1.0.96, <2" +serde_repr = ">=0.1.12, <0.2" +url = ">=2.3.1, <3" +uuid = { version = ">=1.3.3, <2", features = ["serde"] } [dependencies.reqwest] -version = "^0.11.18" +version = ">=0.11.18, <0.12" features = ["json", "multipart"] [dev-dependencies] diff --git a/crates/bitwarden-api-api/README.md b/crates/bitwarden-api-api/README.md index 255041dc5..43cbc40e8 100644 --- a/crates/bitwarden-api-api/README.md +++ b/crates/bitwarden-api-api/README.md @@ -86,6 +86,7 @@ All URIs are relative to _http://localhost_ | _AccountsApi_ | [**accounts_verify_password_post**](docs/AccountsApi.md#accounts_verify_password_post) | **POST** /accounts/verify-password | | _AccountsBillingApi_ | [**accounts_billing_history_get**](docs/AccountsBillingApi.md#accounts_billing_history_get) | **GET** /accounts/billing/history | | _AccountsBillingApi_ | [**accounts_billing_payment_method_get**](docs/AccountsBillingApi.md#accounts_billing_payment_method_get) | **GET** /accounts/billing/payment-method | +| _AuthRequestsApi_ | [**auth_requests_admin_request_post**](docs/AuthRequestsApi.md#auth_requests_admin_request_post) | **POST** /auth-requests/admin-request | | _AuthRequestsApi_ | [**auth_requests_get**](docs/AuthRequestsApi.md#auth_requests_get) | **GET** /auth-requests | | _AuthRequestsApi_ | [**auth_requests_id_get**](docs/AuthRequestsApi.md#auth_requests_id_get) | **GET** /auth-requests/{id} | | _AuthRequestsApi_ | [**auth_requests_id_put**](docs/AuthRequestsApi.md#auth_requests_id_put) | **PUT** /auth-requests/{id} | @@ -162,7 +163,6 @@ All URIs are relative to _http://localhost_ | _CollectionsApi_ | [**organizations_org_id_collections_id_users_put**](docs/CollectionsApi.md#organizations_org_id_collections_id_users_put) | **PUT** /organizations/{orgId}/collections/{id}/users | | _CollectionsApi_ | [**organizations_org_id_collections_post**](docs/CollectionsApi.md#organizations_org_id_collections_post) | **POST** /organizations/{orgId}/collections | | _ConfigApi_ | [**config_get**](docs/ConfigApi.md#config_get) | **GET** /config | -| _DevicesApi_ | [**devices_exist_by_types_post**](docs/DevicesApi.md#devices_exist_by_types_post) | **POST** /devices/exist-by-types | | _DevicesApi_ | [**devices_get**](docs/DevicesApi.md#devices_get) | **GET** /devices | | _DevicesApi_ | [**devices_id_delete**](docs/DevicesApi.md#devices_id_delete) | **DELETE** /devices/{id} | | _DevicesApi_ | [**devices_id_delete_post**](docs/DevicesApi.md#devices_id_delete_post) | **POST** /devices/{id}/delete | @@ -176,9 +176,11 @@ All URIs are relative to _http://localhost_ | _DevicesApi_ | [**devices_identifier_identifier_token_put**](docs/DevicesApi.md#devices_identifier_identifier_token_put) | **PUT** /devices/identifier/{identifier}/token | | _DevicesApi_ | [**devices_identifier_keys_post**](docs/DevicesApi.md#devices_identifier_keys_post) | **POST** /devices/{identifier}/keys | | _DevicesApi_ | [**devices_identifier_keys_put**](docs/DevicesApi.md#devices_identifier_keys_put) | **PUT** /devices/{identifier}/keys | +| _DevicesApi_ | [**devices_identifier_retrieve_keys_post**](docs/DevicesApi.md#devices_identifier_retrieve_keys_post) | **POST** /devices/{identifier}/retrieve-keys | | _DevicesApi_ | [**devices_knowndevice_email_identifier_get**](docs/DevicesApi.md#devices_knowndevice_email_identifier_get) | **GET** /devices/knowndevice/{email}/{identifier} | | _DevicesApi_ | [**devices_knowndevice_get**](docs/DevicesApi.md#devices_knowndevice_get) | **GET** /devices/knowndevice | | _DevicesApi_ | [**devices_post**](docs/DevicesApi.md#devices_post) | **POST** /devices | +| _DevicesApi_ | [**devices_update_trust_post**](docs/DevicesApi.md#devices_update_trust_post) | **POST** /devices/update-trust | | _EmergencyAccessApi_ | [**emergency_access_granted_get**](docs/EmergencyAccessApi.md#emergency_access_granted_get) | **GET** /emergency-access/granted | | _EmergencyAccessApi_ | [**emergency_access_id_accept_post**](docs/EmergencyAccessApi.md#emergency_access_id_accept_post) | **POST** /emergency-access/{id}/accept | | _EmergencyAccessApi_ | [**emergency_access_id_approve_post**](docs/EmergencyAccessApi.md#emergency_access_id_approve_post) | **POST** /emergency-access/{id}/approve | @@ -304,7 +306,6 @@ All URIs are relative to _http://localhost_ | _OrganizationsApi_ | [**organizations_id_cancel_post**](docs/OrganizationsApi.md#organizations_id_cancel_post) | **POST** /organizations/{id}/cancel | | _OrganizationsApi_ | [**organizations_id_delete**](docs/OrganizationsApi.md#organizations_id_delete) | **DELETE** /organizations/{id} | | _OrganizationsApi_ | [**organizations_id_delete_post**](docs/OrganizationsApi.md#organizations_id_delete_post) | **POST** /organizations/{id}/delete | -| _OrganizationsApi_ | [**organizations_id_enroll_secrets_manager_post**](docs/OrganizationsApi.md#organizations_id_enroll_secrets_manager_post) | **POST** /organizations/{id}/enroll-secrets-manager | | _OrganizationsApi_ | [**organizations_id_get**](docs/OrganizationsApi.md#organizations_id_get) | **GET** /organizations/{id} | | _OrganizationsApi_ | [**organizations_id_import_post**](docs/OrganizationsApi.md#organizations_id_import_post) | **POST** /organizations/{id}/import | | _OrganizationsApi_ | [**organizations_id_keys_get**](docs/OrganizationsApi.md#organizations_id_keys_get) | **GET** /organizations/{id}/keys | @@ -313,6 +314,7 @@ All URIs are relative to _http://localhost_ | _OrganizationsApi_ | [**organizations_id_license_get**](docs/OrganizationsApi.md#organizations_id_license_get) | **GET** /organizations/{id}/license | | _OrganizationsApi_ | [**organizations_id_payment_post**](docs/OrganizationsApi.md#organizations_id_payment_post) | **POST** /organizations/{id}/payment | | _OrganizationsApi_ | [**organizations_id_post**](docs/OrganizationsApi.md#organizations_id_post) | **POST** /organizations/{id} | +| _OrganizationsApi_ | [**organizations_id_public_key_get**](docs/OrganizationsApi.md#organizations_id_public_key_get) | **GET** /organizations/{id}/public-key | | _OrganizationsApi_ | [**organizations_id_put**](docs/OrganizationsApi.md#organizations_id_put) | **PUT** /organizations/{id} | | _OrganizationsApi_ | [**organizations_id_reinstate_post**](docs/OrganizationsApi.md#organizations_id_reinstate_post) | **POST** /organizations/{id}/reinstate | | _OrganizationsApi_ | [**organizations_id_rotate_api_key_post**](docs/OrganizationsApi.md#organizations_id_rotate_api_key_post) | **POST** /organizations/{id}/rotate-api-key | @@ -321,6 +323,7 @@ All URIs are relative to _http://localhost_ | _OrganizationsApi_ | [**organizations_id_sso_get**](docs/OrganizationsApi.md#organizations_id_sso_get) | **GET** /organizations/{id}/sso | | _OrganizationsApi_ | [**organizations_id_sso_post**](docs/OrganizationsApi.md#organizations_id_sso_post) | **POST** /organizations/{id}/sso | | _OrganizationsApi_ | [**organizations_id_storage_post**](docs/OrganizationsApi.md#organizations_id_storage_post) | **POST** /organizations/{id}/storage | +| _OrganizationsApi_ | [**organizations_id_subscribe_secrets_manager_post**](docs/OrganizationsApi.md#organizations_id_subscribe_secrets_manager_post) | **POST** /organizations/{id}/subscribe-secrets-manager | | _OrganizationsApi_ | [**organizations_id_subscription_get**](docs/OrganizationsApi.md#organizations_id_subscription_get) | **GET** /organizations/{id}/subscription | | _OrganizationsApi_ | [**organizations_id_subscription_post**](docs/OrganizationsApi.md#organizations_id_subscription_post) | **POST** /organizations/{id}/subscription | | _OrganizationsApi_ | [**organizations_id_tax_get**](docs/OrganizationsApi.md#organizations_id_tax_get) | **GET** /organizations/{id}/tax | @@ -472,6 +475,7 @@ All URIs are relative to _http://localhost_ - [BaseAccessPolicyResponseModel](docs/BaseAccessPolicyResponseModel.md) - [BaseSecretResponseModel](docs/BaseSecretResponseModel.md) - [BaseSecretResponseModelListResponseModel](docs/BaseSecretResponseModelListResponseModel.md) +- [BillingCustomerDiscount](docs/BillingCustomerDiscount.md) - [BillingHistoryResponseModel](docs/BillingHistoryResponseModel.md) - [BillingInvoice](docs/BillingInvoice.md) - [BillingPaymentResponseModel](docs/BillingPaymentResponseModel.md) @@ -525,6 +529,7 @@ All URIs are relative to _http://localhost_ - [ConfigResponseModel](docs/ConfigResponseModel.md) - [DeleteRecoverRequestModel](docs/DeleteRecoverRequestModel.md) - [DeviceKeysRequestModel](docs/DeviceKeysRequestModel.md) +- [DeviceKeysUpdateRequestModel](docs/DeviceKeysUpdateRequestModel.md) - [DeviceRequestModel](docs/DeviceRequestModel.md) - [DeviceResponseModel](docs/DeviceResponseModel.md) - [DeviceResponseModelListResponseModel](docs/DeviceResponseModelListResponseModel.md) @@ -603,10 +608,10 @@ All URIs are relative to _http://localhost_ - [OrganizationDomainResponseModelListResponseModel](docs/OrganizationDomainResponseModelListResponseModel.md) - [OrganizationDomainSsoDetailsRequestModel](docs/OrganizationDomainSsoDetailsRequestModel.md) - [OrganizationDomainSsoDetailsResponseModel](docs/OrganizationDomainSsoDetailsResponseModel.md) -- [OrganizationEnrollSecretsManagerRequestModel](docs/OrganizationEnrollSecretsManagerRequestModel.md) - [OrganizationKeysRequestModel](docs/OrganizationKeysRequestModel.md) - [OrganizationKeysResponseModel](docs/OrganizationKeysResponseModel.md) - [OrganizationLicense](docs/OrganizationLicense.md) +- [OrganizationPublicKeyResponseModel](docs/OrganizationPublicKeyResponseModel.md) - [OrganizationResponseModel](docs/OrganizationResponseModel.md) - [OrganizationSeatRequestModel](docs/OrganizationSeatRequestModel.md) - [OrganizationSponsorshipCreateRequestModel](docs/OrganizationSponsorshipCreateRequestModel.md) @@ -644,6 +649,7 @@ All URIs are relative to _http://localhost_ - [OrganizationUserUserDetailsResponseModel](docs/OrganizationUserUserDetailsResponseModel.md) - [OrganizationUserUserDetailsResponseModelListResponseModel](docs/OrganizationUserUserDetailsResponseModelListResponseModel.md) - [OrganizationVerifyBankRequestModel](docs/OrganizationVerifyBankRequestModel.md) +- [OtherDeviceKeysUpdateRequestModel](docs/OtherDeviceKeysUpdateRequestModel.md) - [PasswordHintRequestModel](docs/PasswordHintRequestModel.md) - [PasswordRequestModel](docs/PasswordRequestModel.md) - [PaymentMethodType](docs/PaymentMethodType.md) @@ -675,6 +681,7 @@ All URIs are relative to _http://localhost_ - [ProjectResponseModel](docs/ProjectResponseModel.md) - [ProjectResponseModelListResponseModel](docs/ProjectResponseModelListResponseModel.md) - [ProjectUpdateRequestModel](docs/ProjectUpdateRequestModel.md) +- [ProtectedDeviceResponseModel](docs/ProtectedDeviceResponseModel.md) - [ProviderOrganizationAddRequestModel](docs/ProviderOrganizationAddRequestModel.md) - [ProviderOrganizationCreateRequestModel](docs/ProviderOrganizationCreateRequestModel.md) - [ProviderOrganizationOrganizationDetailsResponseModel](docs/ProviderOrganizationOrganizationDetailsResponseModel.md) @@ -719,6 +726,7 @@ All URIs are relative to _http://localhost_ - [SecretVerificationRequestModel](docs/SecretVerificationRequestModel.md) - [SecretWithProjectsInnerProject](docs/SecretWithProjectsInnerProject.md) - [SecretWithProjectsListResponseModel](docs/SecretWithProjectsListResponseModel.md) +- [SecretsManagerSubscribeRequestModel](docs/SecretsManagerSubscribeRequestModel.md) - [SecretsManagerSubscriptionUpdateRequestModel](docs/SecretsManagerSubscriptionUpdateRequestModel.md) - [SecretsWithProjectsInnerSecret](docs/SecretsWithProjectsInnerSecret.md) - [SecureNoteType](docs/SecureNoteType.md) @@ -740,7 +748,8 @@ All URIs are relative to _http://localhost_ - [ServiceAccountProjectAccessPolicyResponseModel](docs/ServiceAccountProjectAccessPolicyResponseModel.md) - [ServiceAccountProjectAccessPolicyResponseModelListResponseModel](docs/ServiceAccountProjectAccessPolicyResponseModelListResponseModel.md) - [ServiceAccountResponseModel](docs/ServiceAccountResponseModel.md) -- [ServiceAccountResponseModelListResponseModel](docs/ServiceAccountResponseModelListResponseModel.md) +- [ServiceAccountSecretsDetailsResponseModel](docs/ServiceAccountSecretsDetailsResponseModel.md) +- [ServiceAccountSecretsDetailsResponseModelListResponseModel](docs/ServiceAccountSecretsDetailsResponseModelListResponseModel.md) - [ServiceAccountUpdateRequestModel](docs/ServiceAccountUpdateRequestModel.md) - [SetKeyConnectorKeyRequestModel](docs/SetKeyConnectorKeyRequestModel.md) - [SetPasswordRequestModel](docs/SetPasswordRequestModel.md) @@ -773,6 +782,7 @@ All URIs are relative to _http://localhost_ - [TwoFactorWebAuthnResponseModel](docs/TwoFactorWebAuthnResponseModel.md) - [TwoFactorYubiKeyResponseModel](docs/TwoFactorYubiKeyResponseModel.md) - [UpdateAvatarRequestModel](docs/UpdateAvatarRequestModel.md) +- [UpdateDevicesTrustRequestModel](docs/UpdateDevicesTrustRequestModel.md) - [UpdateDomainsRequestModel](docs/UpdateDomainsRequestModel.md) - [UpdateKeyRequestModel](docs/UpdateKeyRequestModel.md) - [UpdateProfileRequestModel](docs/UpdateProfileRequestModel.md) diff --git a/crates/bitwarden-api-api/src/apis/auth_requests_api.rs b/crates/bitwarden-api-api/src/apis/auth_requests_api.rs index 476a94129..39c4dd7b4 100644 --- a/crates/bitwarden-api-api/src/apis/auth_requests_api.rs +++ b/crates/bitwarden-api-api/src/apis/auth_requests_api.rs @@ -13,6 +13,13 @@ use reqwest; use super::{configuration, Error}; use crate::apis::ResponseContent; +/// struct for typed errors of method [`auth_requests_admin_request_post`] +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(untagged)] +pub enum AuthRequestsAdminRequestPostError { + UnknownValue(serde_json::Value), +} + /// struct for typed errors of method [`auth_requests_get`] #[derive(Debug, Clone, Serialize, Deserialize)] #[serde(untagged)] @@ -48,6 +55,50 @@ pub enum AuthRequestsPostError { UnknownValue(serde_json::Value), } +pub async fn auth_requests_admin_request_post( + configuration: &configuration::Configuration, + auth_request_create_request_model: Option, +) -> Result> { + let local_var_configuration = configuration; + + let local_var_client = &local_var_configuration.client; + + let local_var_uri_str = format!( + "{}/auth-requests/admin-request", + local_var_configuration.base_path + ); + let mut local_var_req_builder = + local_var_client.request(reqwest::Method::POST, local_var_uri_str.as_str()); + + if let Some(ref local_var_user_agent) = local_var_configuration.user_agent { + local_var_req_builder = + local_var_req_builder.header(reqwest::header::USER_AGENT, local_var_user_agent.clone()); + } + if let Some(ref local_var_token) = local_var_configuration.oauth_access_token { + local_var_req_builder = local_var_req_builder.bearer_auth(local_var_token.to_owned()); + }; + local_var_req_builder = local_var_req_builder.json(&auth_request_create_request_model); + + let local_var_req = local_var_req_builder.build()?; + let local_var_resp = local_var_client.execute(local_var_req).await?; + + let local_var_status = local_var_resp.status(); + let local_var_content = local_var_resp.text().await?; + + if !local_var_status.is_client_error() && !local_var_status.is_server_error() { + serde_json::from_str(&local_var_content).map_err(Error::from) + } else { + let local_var_entity: Option = + serde_json::from_str(&local_var_content).ok(); + let local_var_error = ResponseContent { + status: local_var_status, + content: local_var_content, + entity: local_var_entity, + }; + Err(Error::ResponseError(local_var_error)) + } +} + pub async fn auth_requests_get( configuration: &configuration::Configuration, ) -> Result> { diff --git a/crates/bitwarden-api-api/src/apis/devices_api.rs b/crates/bitwarden-api-api/src/apis/devices_api.rs index e7be0685e..95564464a 100644 --- a/crates/bitwarden-api-api/src/apis/devices_api.rs +++ b/crates/bitwarden-api-api/src/apis/devices_api.rs @@ -13,13 +13,6 @@ use reqwest; use super::{configuration, Error}; use crate::apis::ResponseContent; -/// struct for typed errors of method [`devices_exist_by_types_post`] -#[derive(Debug, Clone, Serialize, Deserialize)] -#[serde(untagged)] -pub enum DevicesExistByTypesPostError { - UnknownValue(serde_json::Value), -} - /// struct for typed errors of method [`devices_get`] #[derive(Debug, Clone, Serialize, Deserialize)] #[serde(untagged)] @@ -111,6 +104,13 @@ pub enum DevicesIdentifierKeysPutError { UnknownValue(serde_json::Value), } +/// struct for typed errors of method [`devices_identifier_retrieve_keys_post`] +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(untagged)] +pub enum DevicesIdentifierRetrieveKeysPostError { + UnknownValue(serde_json::Value), +} + /// struct for typed errors of method [`devices_knowndevice_email_identifier_get`] #[derive(Debug, Clone, Serialize, Deserialize)] #[serde(untagged)] @@ -132,48 +132,11 @@ pub enum DevicesPostError { UnknownValue(serde_json::Value), } -pub async fn devices_exist_by_types_post( - configuration: &configuration::Configuration, - device_type: Option>, -) -> Result> { - let local_var_configuration = configuration; - - let local_var_client = &local_var_configuration.client; - - let local_var_uri_str = format!( - "{}/devices/exist-by-types", - local_var_configuration.base_path - ); - let mut local_var_req_builder = - local_var_client.request(reqwest::Method::POST, local_var_uri_str.as_str()); - - if let Some(ref local_var_user_agent) = local_var_configuration.user_agent { - local_var_req_builder = - local_var_req_builder.header(reqwest::header::USER_AGENT, local_var_user_agent.clone()); - } - if let Some(ref local_var_token) = local_var_configuration.oauth_access_token { - local_var_req_builder = local_var_req_builder.bearer_auth(local_var_token.to_owned()); - }; - local_var_req_builder = local_var_req_builder.json(&device_type); - - let local_var_req = local_var_req_builder.build()?; - let local_var_resp = local_var_client.execute(local_var_req).await?; - - let local_var_status = local_var_resp.status(); - let local_var_content = local_var_resp.text().await?; - - if !local_var_status.is_client_error() && !local_var_status.is_server_error() { - serde_json::from_str(&local_var_content).map_err(Error::from) - } else { - let local_var_entity: Option = - serde_json::from_str(&local_var_content).ok(); - let local_var_error = ResponseContent { - status: local_var_status, - content: local_var_content, - entity: local_var_entity, - }; - Err(Error::ResponseError(local_var_error)) - } +/// struct for typed errors of method [`devices_update_trust_post`] +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(untagged)] +pub enum DevicesUpdateTrustPostError { + UnknownValue(serde_json::Value), } pub async fn devices_get( @@ -755,6 +718,55 @@ pub async fn devices_identifier_keys_put( } } +pub async fn devices_identifier_retrieve_keys_post( + configuration: &configuration::Configuration, + identifier: &str, + secret_verification_request_model: Option, +) -> Result< + crate::models::ProtectedDeviceResponseModel, + Error, +> { + let local_var_configuration = configuration; + + let local_var_client = &local_var_configuration.client; + + let local_var_uri_str = format!( + "{}/devices/{identifier}/retrieve-keys", + local_var_configuration.base_path, + identifier = crate::apis::urlencode(identifier.to_string()) + ); + let mut local_var_req_builder = + local_var_client.request(reqwest::Method::POST, local_var_uri_str.as_str()); + + if let Some(ref local_var_user_agent) = local_var_configuration.user_agent { + local_var_req_builder = + local_var_req_builder.header(reqwest::header::USER_AGENT, local_var_user_agent.clone()); + } + if let Some(ref local_var_token) = local_var_configuration.oauth_access_token { + local_var_req_builder = local_var_req_builder.bearer_auth(local_var_token.to_owned()); + }; + local_var_req_builder = local_var_req_builder.json(&secret_verification_request_model); + + let local_var_req = local_var_req_builder.build()?; + let local_var_resp = local_var_client.execute(local_var_req).await?; + + let local_var_status = local_var_resp.status(); + let local_var_content = local_var_resp.text().await?; + + if !local_var_status.is_client_error() && !local_var_status.is_server_error() { + serde_json::from_str(&local_var_content).map_err(Error::from) + } else { + let local_var_entity: Option = + serde_json::from_str(&local_var_content).ok(); + let local_var_error = ResponseContent { + status: local_var_status, + content: local_var_content, + entity: local_var_entity, + }; + Err(Error::ResponseError(local_var_error)) + } +} + pub async fn devices_knowndevice_email_identifier_get( configuration: &configuration::Configuration, email: &str, @@ -890,3 +902,44 @@ pub async fn devices_post( Err(Error::ResponseError(local_var_error)) } } + +pub async fn devices_update_trust_post( + configuration: &configuration::Configuration, + update_devices_trust_request_model: Option, +) -> Result<(), Error> { + let local_var_configuration = configuration; + + let local_var_client = &local_var_configuration.client; + + let local_var_uri_str = format!("{}/devices/update-trust", local_var_configuration.base_path); + let mut local_var_req_builder = + local_var_client.request(reqwest::Method::POST, local_var_uri_str.as_str()); + + if let Some(ref local_var_user_agent) = local_var_configuration.user_agent { + local_var_req_builder = + local_var_req_builder.header(reqwest::header::USER_AGENT, local_var_user_agent.clone()); + } + if let Some(ref local_var_token) = local_var_configuration.oauth_access_token { + local_var_req_builder = local_var_req_builder.bearer_auth(local_var_token.to_owned()); + }; + local_var_req_builder = local_var_req_builder.json(&update_devices_trust_request_model); + + let local_var_req = local_var_req_builder.build()?; + let local_var_resp = local_var_client.execute(local_var_req).await?; + + let local_var_status = local_var_resp.status(); + let local_var_content = local_var_resp.text().await?; + + if !local_var_status.is_client_error() && !local_var_status.is_server_error() { + Ok(()) + } else { + let local_var_entity: Option = + serde_json::from_str(&local_var_content).ok(); + let local_var_error = ResponseContent { + status: local_var_status, + content: local_var_content, + entity: local_var_entity, + }; + Err(Error::ResponseError(local_var_error)) + } +} diff --git a/crates/bitwarden-api-api/src/apis/mod.rs b/crates/bitwarden-api-api/src/apis/mod.rs index 623d5d0b6..492680582 100644 --- a/crates/bitwarden-api-api/src/apis/mod.rs +++ b/crates/bitwarden-api-api/src/apis/mod.rs @@ -1,5 +1,4 @@ -use std::error; -use std::fmt; +use std::{error, fmt}; #[derive(Debug, Clone)] pub struct ResponseContent { diff --git a/crates/bitwarden-api-api/src/apis/organization_users_api.rs b/crates/bitwarden-api-api/src/apis/organization_users_api.rs index f81290a86..5a8e99916 100644 --- a/crates/bitwarden-api-api/src/apis/organization_users_api.rs +++ b/crates/bitwarden-api-api/src/apis/organization_users_api.rs @@ -391,10 +391,7 @@ pub async fn organizations_org_id_users_enable_secrets_manager_patch( configuration: &configuration::Configuration, org_id: uuid::Uuid, organization_user_bulk_request_model: Option, -) -> Result< - crate::models::OrganizationUserBulkResponseModelListResponseModel, - Error, -> { +) -> Result<(), Error> { let local_var_configuration = configuration; let local_var_client = &local_var_configuration.client; @@ -423,7 +420,7 @@ pub async fn organizations_org_id_users_enable_secrets_manager_patch( let local_var_content = local_var_resp.text().await?; if !local_var_status.is_client_error() && !local_var_status.is_server_error() { - serde_json::from_str(&local_var_content).map_err(Error::from) + Ok(()) } else { let local_var_entity: Option = serde_json::from_str(&local_var_content).ok(); @@ -440,10 +437,7 @@ pub async fn organizations_org_id_users_enable_secrets_manager_put( configuration: &configuration::Configuration, org_id: uuid::Uuid, organization_user_bulk_request_model: Option, -) -> Result< - crate::models::OrganizationUserBulkResponseModelListResponseModel, - Error, -> { +) -> Result<(), Error> { let local_var_configuration = configuration; let local_var_client = &local_var_configuration.client; @@ -472,7 +466,7 @@ pub async fn organizations_org_id_users_enable_secrets_manager_put( let local_var_content = local_var_resp.text().await?; if !local_var_status.is_client_error() && !local_var_status.is_server_error() { - serde_json::from_str(&local_var_content).map_err(Error::from) + Ok(()) } else { let local_var_entity: Option = serde_json::from_str(&local_var_content).ok(); @@ -1761,8 +1755,8 @@ pub async fn organizations_org_id_users_revoke_put( pub async fn organizations_org_id_users_user_id_reset_password_enrollment_put( configuration: &configuration::Configuration, - org_id: &str, - user_id: &str, + org_id: uuid::Uuid, + user_id: uuid::Uuid, organization_user_reset_password_enrollment_request_model: Option< crate::models::OrganizationUserResetPasswordEnrollmentRequestModel, >, diff --git a/crates/bitwarden-api-api/src/apis/organizations_api.rs b/crates/bitwarden-api-api/src/apis/organizations_api.rs index 1cd531b2f..6377ef6bf 100644 --- a/crates/bitwarden-api-api/src/apis/organizations_api.rs +++ b/crates/bitwarden-api-api/src/apis/organizations_api.rs @@ -62,13 +62,6 @@ pub enum OrganizationsIdDeletePostError { UnknownValue(serde_json::Value), } -/// struct for typed errors of method [`organizations_id_enroll_secrets_manager_post`] -#[derive(Debug, Clone, Serialize, Deserialize)] -#[serde(untagged)] -pub enum OrganizationsIdEnrollSecretsManagerPostError { - UnknownValue(serde_json::Value), -} - /// struct for typed errors of method [`organizations_id_get`] #[derive(Debug, Clone, Serialize, Deserialize)] #[serde(untagged)] @@ -125,6 +118,13 @@ pub enum OrganizationsIdPostError { UnknownValue(serde_json::Value), } +/// struct for typed errors of method [`organizations_id_public_key_get`] +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(untagged)] +pub enum OrganizationsIdPublicKeyGetError { + UnknownValue(serde_json::Value), +} + /// struct for typed errors of method [`organizations_id_put`] #[derive(Debug, Clone, Serialize, Deserialize)] #[serde(untagged)] @@ -181,6 +181,13 @@ pub enum OrganizationsIdStoragePostError { UnknownValue(serde_json::Value), } +/// struct for typed errors of method [`organizations_id_subscribe_secrets_manager_post`] +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(untagged)] +pub enum OrganizationsIdSubscribeSecretsManagerPostError { + UnknownValue(serde_json::Value), +} + /// struct for typed errors of method [`organizations_id_subscription_get`] #[derive(Debug, Clone, Serialize, Deserialize)] #[serde(untagged)] @@ -549,55 +556,6 @@ pub async fn organizations_id_delete_post( } } -pub async fn organizations_id_enroll_secrets_manager_post( - configuration: &configuration::Configuration, - id: uuid::Uuid, - organization_enroll_secrets_manager_request_model: Option< - crate::models::OrganizationEnrollSecretsManagerRequestModel, - >, -) -> Result<(), Error> { - let local_var_configuration = configuration; - - let local_var_client = &local_var_configuration.client; - - let local_var_uri_str = format!( - "{}/organizations/{id}/enroll-secrets-manager", - local_var_configuration.base_path, - id = crate::apis::urlencode(id.to_string()) - ); - let mut local_var_req_builder = - local_var_client.request(reqwest::Method::POST, local_var_uri_str.as_str()); - - if let Some(ref local_var_user_agent) = local_var_configuration.user_agent { - local_var_req_builder = - local_var_req_builder.header(reqwest::header::USER_AGENT, local_var_user_agent.clone()); - } - if let Some(ref local_var_token) = local_var_configuration.oauth_access_token { - local_var_req_builder = local_var_req_builder.bearer_auth(local_var_token.to_owned()); - }; - local_var_req_builder = - local_var_req_builder.json(&organization_enroll_secrets_manager_request_model); - - let local_var_req = local_var_req_builder.build()?; - let local_var_resp = local_var_client.execute(local_var_req).await?; - - let local_var_status = local_var_resp.status(); - let local_var_content = local_var_resp.text().await?; - - if !local_var_status.is_client_error() && !local_var_status.is_server_error() { - Ok(()) - } else { - let local_var_entity: Option = - serde_json::from_str(&local_var_content).ok(); - let local_var_error = ResponseContent { - status: local_var_status, - content: local_var_content, - entity: local_var_entity, - }; - Err(Error::ResponseError(local_var_error)) - } -} - pub async fn organizations_id_get( configuration: &configuration::Configuration, id: &str, @@ -693,7 +651,7 @@ pub async fn organizations_id_import_post( pub async fn organizations_id_keys_get( configuration: &configuration::Configuration, id: &str, -) -> Result> { +) -> Result> { let local_var_configuration = configuration; let local_var_client = &local_var_configuration.client; @@ -965,6 +923,53 @@ pub async fn organizations_id_post( } } +pub async fn organizations_id_public_key_get( + configuration: &configuration::Configuration, + id: &str, +) -> Result< + crate::models::OrganizationPublicKeyResponseModel, + Error, +> { + let local_var_configuration = configuration; + + let local_var_client = &local_var_configuration.client; + + let local_var_uri_str = format!( + "{}/organizations/{id}/public-key", + local_var_configuration.base_path, + id = crate::apis::urlencode(id.to_string()) + ); + let mut local_var_req_builder = + local_var_client.request(reqwest::Method::GET, local_var_uri_str.as_str()); + + if let Some(ref local_var_user_agent) = local_var_configuration.user_agent { + local_var_req_builder = + local_var_req_builder.header(reqwest::header::USER_AGENT, local_var_user_agent.clone()); + } + if let Some(ref local_var_token) = local_var_configuration.oauth_access_token { + local_var_req_builder = local_var_req_builder.bearer_auth(local_var_token.to_owned()); + }; + + let local_var_req = local_var_req_builder.build()?; + let local_var_resp = local_var_client.execute(local_var_req).await?; + + let local_var_status = local_var_resp.status(); + let local_var_content = local_var_resp.text().await?; + + if !local_var_status.is_client_error() && !local_var_status.is_server_error() { + serde_json::from_str(&local_var_content).map_err(Error::from) + } else { + let local_var_entity: Option = + serde_json::from_str(&local_var_content).ok(); + let local_var_error = ResponseContent { + status: local_var_status, + content: local_var_content, + entity: local_var_entity, + }; + Err(Error::ResponseError(local_var_error)) + } +} + pub async fn organizations_id_put( configuration: &configuration::Configuration, id: &str, @@ -1332,6 +1337,57 @@ pub async fn organizations_id_storage_post( } } +pub async fn organizations_id_subscribe_secrets_manager_post( + configuration: &configuration::Configuration, + id: uuid::Uuid, + secrets_manager_subscribe_request_model: Option< + crate::models::SecretsManagerSubscribeRequestModel, + >, +) -> Result< + crate::models::ProfileOrganizationResponseModel, + Error, +> { + let local_var_configuration = configuration; + + let local_var_client = &local_var_configuration.client; + + let local_var_uri_str = format!( + "{}/organizations/{id}/subscribe-secrets-manager", + local_var_configuration.base_path, + id = crate::apis::urlencode(id.to_string()) + ); + let mut local_var_req_builder = + local_var_client.request(reqwest::Method::POST, local_var_uri_str.as_str()); + + if let Some(ref local_var_user_agent) = local_var_configuration.user_agent { + local_var_req_builder = + local_var_req_builder.header(reqwest::header::USER_AGENT, local_var_user_agent.clone()); + } + if let Some(ref local_var_token) = local_var_configuration.oauth_access_token { + local_var_req_builder = local_var_req_builder.bearer_auth(local_var_token.to_owned()); + }; + local_var_req_builder = local_var_req_builder.json(&secrets_manager_subscribe_request_model); + + let local_var_req = local_var_req_builder.build()?; + let local_var_resp = local_var_client.execute(local_var_req).await?; + + let local_var_status = local_var_resp.status(); + let local_var_content = local_var_resp.text().await?; + + if !local_var_status.is_client_error() && !local_var_status.is_server_error() { + serde_json::from_str(&local_var_content).map_err(Error::from) + } else { + let local_var_entity: Option = + serde_json::from_str(&local_var_content).ok(); + let local_var_error = ResponseContent { + status: local_var_status, + content: local_var_content, + entity: local_var_entity, + }; + Err(Error::ResponseError(local_var_error)) + } +} + pub async fn organizations_id_subscription_get( configuration: &configuration::Configuration, id: &str, diff --git a/crates/bitwarden-api-api/src/apis/service_accounts_api.rs b/crates/bitwarden-api-api/src/apis/service_accounts_api.rs index 511fb3674..47045b475 100644 --- a/crates/bitwarden-api-api/src/apis/service_accounts_api.rs +++ b/crates/bitwarden-api-api/src/apis/service_accounts_api.rs @@ -72,8 +72,9 @@ pub enum ServiceAccountsIdPutError { pub async fn organizations_organization_id_service_accounts_get( configuration: &configuration::Configuration, organization_id: uuid::Uuid, + include_access_to_secrets: Option, ) -> Result< - crate::models::ServiceAccountResponseModelListResponseModel, + crate::models::ServiceAccountSecretsDetailsResponseModelListResponseModel, Error, > { let local_var_configuration = configuration; @@ -88,6 +89,10 @@ pub async fn organizations_organization_id_service_accounts_get( let mut local_var_req_builder = local_var_client.request(reqwest::Method::GET, local_var_uri_str.as_str()); + if let Some(ref local_var_str) = include_access_to_secrets { + local_var_req_builder = + local_var_req_builder.query(&[("includeAccessToSecrets", &local_var_str.to_string())]); + } if let Some(ref local_var_user_agent) = local_var_configuration.user_agent { local_var_req_builder = local_var_req_builder.header(reqwest::header::USER_AGENT, local_var_user_agent.clone()); diff --git a/crates/bitwarden-api-api/src/models/attachment_response_model.rs b/crates/bitwarden-api-api/src/models/attachment_response_model.rs index 86637a161..c9066488d 100644 --- a/crates/bitwarden-api-api/src/models/attachment_response_model.rs +++ b/crates/bitwarden-api-api/src/models/attachment_response_model.rs @@ -21,7 +21,7 @@ pub struct AttachmentResponseModel { #[serde(rename = "key", skip_serializing_if = "Option::is_none")] pub key: Option, #[serde(rename = "size", skip_serializing_if = "Option::is_none")] - pub size: Option, + pub size: Option, #[serde(rename = "sizeName", skip_serializing_if = "Option::is_none")] pub size_name: Option, } diff --git a/crates/bitwarden-api-api/src/models/billing_customer_discount.rs b/crates/bitwarden-api-api/src/models/billing_customer_discount.rs new file mode 100644 index 000000000..61f695d70 --- /dev/null +++ b/crates/bitwarden-api-api/src/models/billing_customer_discount.rs @@ -0,0 +1,26 @@ +/* + * Bitwarden Internal API + * + * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator) + * + * The version of the OpenAPI document: latest + * + * Generated by: https://openapi-generator.tech + */ + +#[derive(Clone, Debug, PartialEq, Default, Serialize, Deserialize)] +pub struct BillingCustomerDiscount { + #[serde(rename = "id", skip_serializing_if = "Option::is_none")] + pub id: Option, + #[serde(rename = "active", skip_serializing_if = "Option::is_none")] + pub active: Option, +} + +impl BillingCustomerDiscount { + pub fn new() -> BillingCustomerDiscount { + BillingCustomerDiscount { + id: None, + active: None, + } + } +} diff --git a/crates/bitwarden-api-api/src/models/cipher_details_response_model.rs b/crates/bitwarden-api-api/src/models/cipher_details_response_model.rs index 448712702..289169e41 100644 --- a/crates/bitwarden-api-api/src/models/cipher_details_response_model.rs +++ b/crates/bitwarden-api-api/src/models/cipher_details_response_model.rs @@ -51,6 +51,8 @@ pub struct CipherDetailsResponseModel { pub deleted_date: Option, #[serde(rename = "reprompt", skip_serializing_if = "Option::is_none")] pub reprompt: Option, + #[serde(rename = "key", skip_serializing_if = "Option::is_none")] + pub key: Option, #[serde(rename = "folderId", skip_serializing_if = "Option::is_none")] pub folder_id: Option, #[serde(rename = "favorite", skip_serializing_if = "Option::is_none")] @@ -85,6 +87,7 @@ impl CipherDetailsResponseModel { creation_date: None, deleted_date: None, reprompt: None, + key: None, folder_id: None, favorite: None, edit: None, diff --git a/crates/bitwarden-api-api/src/models/cipher_mini_details_response_model.rs b/crates/bitwarden-api-api/src/models/cipher_mini_details_response_model.rs index 28dafcdbb..1aa5b8e3b 100644 --- a/crates/bitwarden-api-api/src/models/cipher_mini_details_response_model.rs +++ b/crates/bitwarden-api-api/src/models/cipher_mini_details_response_model.rs @@ -51,6 +51,8 @@ pub struct CipherMiniDetailsResponseModel { pub deleted_date: Option, #[serde(rename = "reprompt", skip_serializing_if = "Option::is_none")] pub reprompt: Option, + #[serde(rename = "key", skip_serializing_if = "Option::is_none")] + pub key: Option, #[serde(rename = "collectionIds", skip_serializing_if = "Option::is_none")] pub collection_ids: Option>, } @@ -77,6 +79,7 @@ impl CipherMiniDetailsResponseModel { creation_date: None, deleted_date: None, reprompt: None, + key: None, collection_ids: None, } } diff --git a/crates/bitwarden-api-api/src/models/cipher_mini_response_model.rs b/crates/bitwarden-api-api/src/models/cipher_mini_response_model.rs index 8ea52926d..c96ddfd18 100644 --- a/crates/bitwarden-api-api/src/models/cipher_mini_response_model.rs +++ b/crates/bitwarden-api-api/src/models/cipher_mini_response_model.rs @@ -51,6 +51,8 @@ pub struct CipherMiniResponseModel { pub deleted_date: Option, #[serde(rename = "reprompt", skip_serializing_if = "Option::is_none")] pub reprompt: Option, + #[serde(rename = "key", skip_serializing_if = "Option::is_none")] + pub key: Option, } impl CipherMiniResponseModel { @@ -75,6 +77,7 @@ impl CipherMiniResponseModel { creation_date: None, deleted_date: None, reprompt: None, + key: None, } } } diff --git a/crates/bitwarden-api-api/src/models/cipher_request_model.rs b/crates/bitwarden-api-api/src/models/cipher_request_model.rs index 4ac3f62d7..2e445802b 100644 --- a/crates/bitwarden-api-api/src/models/cipher_request_model.rs +++ b/crates/bitwarden-api-api/src/models/cipher_request_model.rs @@ -20,6 +20,8 @@ pub struct CipherRequestModel { pub favorite: Option, #[serde(rename = "reprompt", skip_serializing_if = "Option::is_none")] pub reprompt: Option, + #[serde(rename = "key", skip_serializing_if = "Option::is_none")] + pub key: Option, #[serde(rename = "name")] pub name: String, #[serde(rename = "notes", skip_serializing_if = "Option::is_none")] @@ -56,6 +58,7 @@ impl CipherRequestModel { folder_id: None, favorite: None, reprompt: None, + key: None, name, notes: None, fields: None, diff --git a/crates/bitwarden-api-api/src/models/cipher_response_model.rs b/crates/bitwarden-api-api/src/models/cipher_response_model.rs index 62a930e1f..59831e6fb 100644 --- a/crates/bitwarden-api-api/src/models/cipher_response_model.rs +++ b/crates/bitwarden-api-api/src/models/cipher_response_model.rs @@ -51,6 +51,8 @@ pub struct CipherResponseModel { pub deleted_date: Option, #[serde(rename = "reprompt", skip_serializing_if = "Option::is_none")] pub reprompt: Option, + #[serde(rename = "key", skip_serializing_if = "Option::is_none")] + pub key: Option, #[serde(rename = "folderId", skip_serializing_if = "Option::is_none")] pub folder_id: Option, #[serde(rename = "favorite", skip_serializing_if = "Option::is_none")] @@ -83,6 +85,7 @@ impl CipherResponseModel { creation_date: None, deleted_date: None, reprompt: None, + key: None, folder_id: None, favorite: None, edit: None, diff --git a/crates/bitwarden-api-api/src/models/cipher_with_id_request_model.rs b/crates/bitwarden-api-api/src/models/cipher_with_id_request_model.rs index db4c3604f..093d2e5d9 100644 --- a/crates/bitwarden-api-api/src/models/cipher_with_id_request_model.rs +++ b/crates/bitwarden-api-api/src/models/cipher_with_id_request_model.rs @@ -20,6 +20,8 @@ pub struct CipherWithIdRequestModel { pub favorite: Option, #[serde(rename = "reprompt", skip_serializing_if = "Option::is_none")] pub reprompt: Option, + #[serde(rename = "key", skip_serializing_if = "Option::is_none")] + pub key: Option, #[serde(rename = "name")] pub name: String, #[serde(rename = "notes", skip_serializing_if = "Option::is_none")] @@ -58,6 +60,7 @@ impl CipherWithIdRequestModel { folder_id: None, favorite: None, reprompt: None, + key: None, name, notes: None, fields: None, diff --git a/crates/bitwarden-api-api/src/models/device_keys_update_request_model.rs b/crates/bitwarden-api-api/src/models/device_keys_update_request_model.rs new file mode 100644 index 000000000..65d82513c --- /dev/null +++ b/crates/bitwarden-api-api/src/models/device_keys_update_request_model.rs @@ -0,0 +1,29 @@ +/* + * Bitwarden Internal API + * + * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator) + * + * The version of the OpenAPI document: latest + * + * Generated by: https://openapi-generator.tech + */ + +#[derive(Clone, Debug, PartialEq, Default, Serialize, Deserialize)] +pub struct DeviceKeysUpdateRequestModel { + #[serde(rename = "encryptedPublicKey")] + pub encrypted_public_key: String, + #[serde(rename = "encryptedUserKey")] + pub encrypted_user_key: String, +} + +impl DeviceKeysUpdateRequestModel { + pub fn new( + encrypted_public_key: String, + encrypted_user_key: String, + ) -> DeviceKeysUpdateRequestModel { + DeviceKeysUpdateRequestModel { + encrypted_public_key, + encrypted_user_key, + } + } +} diff --git a/crates/bitwarden-api-api/src/models/device_response_model.rs b/crates/bitwarden-api-api/src/models/device_response_model.rs index 463d6f104..c6dc76d96 100644 --- a/crates/bitwarden-api-api/src/models/device_response_model.rs +++ b/crates/bitwarden-api-api/src/models/device_response_model.rs @@ -22,15 +22,8 @@ pub struct DeviceResponseModel { pub identifier: Option, #[serde(rename = "creationDate", skip_serializing_if = "Option::is_none")] pub creation_date: Option, - #[serde(rename = "encryptedUserKey", skip_serializing_if = "Option::is_none")] - pub encrypted_user_key: Option, - #[serde(rename = "encryptedPublicKey", skip_serializing_if = "Option::is_none")] - pub encrypted_public_key: Option, - #[serde( - rename = "encryptedPrivateKey", - skip_serializing_if = "Option::is_none" - )] - pub encrypted_private_key: Option, + #[serde(rename = "isTrusted", skip_serializing_if = "Option::is_none")] + pub is_trusted: Option, } impl DeviceResponseModel { @@ -42,9 +35,7 @@ impl DeviceResponseModel { r#type: None, identifier: None, creation_date: None, - encrypted_user_key: None, - encrypted_public_key: None, - encrypted_private_key: None, + is_trusted: None, } } } diff --git a/crates/bitwarden-api-api/src/models/event_type.rs b/crates/bitwarden-api-api/src/models/event_type.rs index 3988c656f..f0cea8ebc 100644 --- a/crates/bitwarden-api-api/src/models/event_type.rs +++ b/crates/bitwarden-api-api/src/models/event_type.rs @@ -24,6 +24,7 @@ pub enum EventType { Variant1007 = 1007, Variant1008 = 1008, Variant1009 = 1009, + Variant1010 = 1010, Variant1100 = 1100, Variant1101 = 1101, Variant1102 = 1102, @@ -61,6 +62,8 @@ pub enum EventType { Variant1510 = 1510, Variant1511 = 1511, Variant1512 = 1512, + Variant1513 = 1513, + Variant1514 = 1514, Variant1600 = 1600, Variant1601 = 1601, Variant1602 = 1602, @@ -99,6 +102,7 @@ impl ToString for EventType { Self::Variant1007 => String::from("1007"), Self::Variant1008 => String::from("1008"), Self::Variant1009 => String::from("1009"), + Self::Variant1010 => String::from("1010"), Self::Variant1100 => String::from("1100"), Self::Variant1101 => String::from("1101"), Self::Variant1102 => String::from("1102"), @@ -136,6 +140,8 @@ impl ToString for EventType { Self::Variant1510 => String::from("1510"), Self::Variant1511 => String::from("1511"), Self::Variant1512 => String::from("1512"), + Self::Variant1513 => String::from("1513"), + Self::Variant1514 => String::from("1514"), Self::Variant1600 => String::from("1600"), Self::Variant1601 => String::from("1601"), Self::Variant1602 => String::from("1602"), diff --git a/crates/bitwarden-api-api/src/models/global_domains.rs b/crates/bitwarden-api-api/src/models/global_domains.rs index f0582caa4..10a943482 100644 --- a/crates/bitwarden-api-api/src/models/global_domains.rs +++ b/crates/bitwarden-api-api/src/models/global_domains.rs @@ -11,7 +11,7 @@ #[derive(Clone, Debug, PartialEq, Default, Serialize, Deserialize)] pub struct GlobalDomains { #[serde(rename = "type", skip_serializing_if = "Option::is_none")] - pub r#type: Option, + pub r#type: Option, #[serde(rename = "domains", skip_serializing_if = "Option::is_none")] pub domains: Option>, #[serde(rename = "excluded", skip_serializing_if = "Option::is_none")] diff --git a/crates/bitwarden-api-api/src/models/mod.rs b/crates/bitwarden-api-api/src/models/mod.rs index 9d2aa9ae8..db4702827 100644 --- a/crates/bitwarden-api-api/src/models/mod.rs +++ b/crates/bitwarden-api-api/src/models/mod.rs @@ -42,6 +42,8 @@ pub mod base_secret_response_model; pub use self::base_secret_response_model::BaseSecretResponseModel; pub mod base_secret_response_model_list_response_model; pub use self::base_secret_response_model_list_response_model::BaseSecretResponseModelListResponseModel; +pub mod billing_customer_discount; +pub use self::billing_customer_discount::BillingCustomerDiscount; pub mod billing_history_response_model; pub use self::billing_history_response_model::BillingHistoryResponseModel; pub mod billing_invoice; @@ -148,6 +150,8 @@ pub mod delete_recover_request_model; pub use self::delete_recover_request_model::DeleteRecoverRequestModel; pub mod device_keys_request_model; pub use self::device_keys_request_model::DeviceKeysRequestModel; +pub mod device_keys_update_request_model; +pub use self::device_keys_update_request_model::DeviceKeysUpdateRequestModel; pub mod device_request_model; pub use self::device_request_model::DeviceRequestModel; pub mod device_response_model; @@ -304,14 +308,14 @@ pub mod organization_domain_sso_details_request_model; pub use self::organization_domain_sso_details_request_model::OrganizationDomainSsoDetailsRequestModel; pub mod organization_domain_sso_details_response_model; pub use self::organization_domain_sso_details_response_model::OrganizationDomainSsoDetailsResponseModel; -pub mod organization_enroll_secrets_manager_request_model; -pub use self::organization_enroll_secrets_manager_request_model::OrganizationEnrollSecretsManagerRequestModel; pub mod organization_keys_request_model; pub use self::organization_keys_request_model::OrganizationKeysRequestModel; pub mod organization_keys_response_model; pub use self::organization_keys_response_model::OrganizationKeysResponseModel; pub mod organization_license; pub use self::organization_license::OrganizationLicense; +pub mod organization_public_key_response_model; +pub use self::organization_public_key_response_model::OrganizationPublicKeyResponseModel; pub mod organization_response_model; pub use self::organization_response_model::OrganizationResponseModel; pub mod organization_seat_request_model; @@ -386,6 +390,8 @@ pub mod organization_user_user_details_response_model_list_response_model; pub use self::organization_user_user_details_response_model_list_response_model::OrganizationUserUserDetailsResponseModelListResponseModel; pub mod organization_verify_bank_request_model; pub use self::organization_verify_bank_request_model::OrganizationVerifyBankRequestModel; +pub mod other_device_keys_update_request_model; +pub use self::other_device_keys_update_request_model::OtherDeviceKeysUpdateRequestModel; pub mod password_hint_request_model; pub use self::password_hint_request_model::PasswordHintRequestModel; pub mod password_request_model; @@ -448,6 +454,8 @@ pub mod project_response_model_list_response_model; pub use self::project_response_model_list_response_model::ProjectResponseModelListResponseModel; pub mod project_update_request_model; pub use self::project_update_request_model::ProjectUpdateRequestModel; +pub mod protected_device_response_model; +pub use self::protected_device_response_model::ProtectedDeviceResponseModel; pub mod provider_organization_add_request_model; pub use self::provider_organization_add_request_model::ProviderOrganizationAddRequestModel; pub mod provider_organization_create_request_model; @@ -536,6 +544,8 @@ pub mod secret_with_projects_inner_project; pub use self::secret_with_projects_inner_project::SecretWithProjectsInnerProject; pub mod secret_with_projects_list_response_model; pub use self::secret_with_projects_list_response_model::SecretWithProjectsListResponseModel; +pub mod secrets_manager_subscribe_request_model; +pub use self::secrets_manager_subscribe_request_model::SecretsManagerSubscribeRequestModel; pub mod secrets_manager_subscription_update_request_model; pub use self::secrets_manager_subscription_update_request_model::SecretsManagerSubscriptionUpdateRequestModel; pub mod secrets_with_projects_inner_secret; @@ -578,8 +588,10 @@ pub mod service_account_project_access_policy_response_model_list_response_model pub use self::service_account_project_access_policy_response_model_list_response_model::ServiceAccountProjectAccessPolicyResponseModelListResponseModel; pub mod service_account_response_model; pub use self::service_account_response_model::ServiceAccountResponseModel; -pub mod service_account_response_model_list_response_model; -pub use self::service_account_response_model_list_response_model::ServiceAccountResponseModelListResponseModel; +pub mod service_account_secrets_details_response_model; +pub use self::service_account_secrets_details_response_model::ServiceAccountSecretsDetailsResponseModel; +pub mod service_account_secrets_details_response_model_list_response_model; +pub use self::service_account_secrets_details_response_model_list_response_model::ServiceAccountSecretsDetailsResponseModelListResponseModel; pub mod service_account_update_request_model; pub use self::service_account_update_request_model::ServiceAccountUpdateRequestModel; pub mod set_key_connector_key_request_model; @@ -644,6 +656,8 @@ pub mod two_factor_yubi_key_response_model; pub use self::two_factor_yubi_key_response_model::TwoFactorYubiKeyResponseModel; pub mod update_avatar_request_model; pub use self::update_avatar_request_model::UpdateAvatarRequestModel; +pub mod update_devices_trust_request_model; +pub use self::update_devices_trust_request_model::UpdateDevicesTrustRequestModel; pub mod update_domains_request_model; pub use self::update_domains_request_model::UpdateDomainsRequestModel; pub mod update_key_request_model; diff --git a/crates/bitwarden-api-api/src/models/organization_public_key_response_model.rs b/crates/bitwarden-api-api/src/models/organization_public_key_response_model.rs new file mode 100644 index 000000000..c5849d3e6 --- /dev/null +++ b/crates/bitwarden-api-api/src/models/organization_public_key_response_model.rs @@ -0,0 +1,26 @@ +/* + * Bitwarden Internal API + * + * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator) + * + * The version of the OpenAPI document: latest + * + * Generated by: https://openapi-generator.tech + */ + +#[derive(Clone, Debug, PartialEq, Default, Serialize, Deserialize)] +pub struct OrganizationPublicKeyResponseModel { + #[serde(rename = "object", skip_serializing_if = "Option::is_none")] + pub object: Option, + #[serde(rename = "publicKey", skip_serializing_if = "Option::is_none")] + pub public_key: Option, +} + +impl OrganizationPublicKeyResponseModel { + pub fn new() -> OrganizationPublicKeyResponseModel { + OrganizationPublicKeyResponseModel { + object: None, + public_key: None, + } + } +} diff --git a/crates/bitwarden-api-api/src/models/organization_subscription_response_model.rs b/crates/bitwarden-api-api/src/models/organization_subscription_response_model.rs index c3789f17a..b08184d43 100644 --- a/crates/bitwarden-api-api/src/models/organization_subscription_response_model.rs +++ b/crates/bitwarden-api-api/src/models/organization_subscription_response_model.rs @@ -102,6 +102,8 @@ pub struct OrganizationSubscriptionResponseModel { pub storage_name: Option, #[serde(rename = "storageGb", skip_serializing_if = "Option::is_none")] pub storage_gb: Option, + #[serde(rename = "discount", skip_serializing_if = "Option::is_none")] + pub discount: Option>, #[serde(rename = "subscription", skip_serializing_if = "Option::is_none")] pub subscription: Option>, #[serde(rename = "upcomingInvoice", skip_serializing_if = "Option::is_none")] @@ -162,6 +164,7 @@ impl OrganizationSubscriptionResponseModel { max_autoscale_sm_service_accounts: None, storage_name: None, storage_gb: None, + discount: None, subscription: None, upcoming_invoice: None, expiration_without_grace_period: None, diff --git a/crates/bitwarden-api-api/src/models/organization_user_reset_password_enrollment_request_model.rs b/crates/bitwarden-api-api/src/models/organization_user_reset_password_enrollment_request_model.rs index c21020f96..7287c58a3 100644 --- a/crates/bitwarden-api-api/src/models/organization_user_reset_password_enrollment_request_model.rs +++ b/crates/bitwarden-api-api/src/models/organization_user_reset_password_enrollment_request_model.rs @@ -10,17 +10,6 @@ #[derive(Clone, Debug, PartialEq, Default, Serialize, Deserialize)] pub struct OrganizationUserResetPasswordEnrollmentRequestModel { - #[serde(rename = "masterPasswordHash", skip_serializing_if = "Option::is_none")] - pub master_password_hash: Option, - #[serde(rename = "otp", skip_serializing_if = "Option::is_none")] - pub otp: Option, - #[serde( - rename = "authRequestAccessCode", - skip_serializing_if = "Option::is_none" - )] - pub auth_request_access_code: Option, - #[serde(rename = "secret", skip_serializing_if = "Option::is_none")] - pub secret: Option, #[serde(rename = "resetPasswordKey", skip_serializing_if = "Option::is_none")] pub reset_password_key: Option, } @@ -28,10 +17,6 @@ pub struct OrganizationUserResetPasswordEnrollmentRequestModel { impl OrganizationUserResetPasswordEnrollmentRequestModel { pub fn new() -> OrganizationUserResetPasswordEnrollmentRequestModel { OrganizationUserResetPasswordEnrollmentRequestModel { - master_password_hash: None, - otp: None, - auth_request_access_code: None, - secret: None, reset_password_key: None, } } diff --git a/crates/bitwarden-api-api/src/models/other_device_keys_update_request_model.rs b/crates/bitwarden-api-api/src/models/other_device_keys_update_request_model.rs new file mode 100644 index 000000000..c163a4343 --- /dev/null +++ b/crates/bitwarden-api-api/src/models/other_device_keys_update_request_model.rs @@ -0,0 +1,33 @@ +/* + * Bitwarden Internal API + * + * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator) + * + * The version of the OpenAPI document: latest + * + * Generated by: https://openapi-generator.tech + */ + +#[derive(Clone, Debug, PartialEq, Default, Serialize, Deserialize)] +pub struct OtherDeviceKeysUpdateRequestModel { + #[serde(rename = "encryptedPublicKey")] + pub encrypted_public_key: String, + #[serde(rename = "encryptedUserKey")] + pub encrypted_user_key: String, + #[serde(rename = "deviceId")] + pub device_id: uuid::Uuid, +} + +impl OtherDeviceKeysUpdateRequestModel { + pub fn new( + encrypted_public_key: String, + encrypted_user_key: String, + device_id: uuid::Uuid, + ) -> OtherDeviceKeysUpdateRequestModel { + OtherDeviceKeysUpdateRequestModel { + encrypted_public_key, + encrypted_user_key, + device_id, + } + } +} diff --git a/crates/bitwarden-api-api/src/models/protected_device_response_model.rs b/crates/bitwarden-api-api/src/models/protected_device_response_model.rs new file mode 100644 index 000000000..f34e1128d --- /dev/null +++ b/crates/bitwarden-api-api/src/models/protected_device_response_model.rs @@ -0,0 +1,44 @@ +/* + * Bitwarden Internal API + * + * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator) + * + * The version of the OpenAPI document: latest + * + * Generated by: https://openapi-generator.tech + */ + +#[derive(Clone, Debug, PartialEq, Default, Serialize, Deserialize)] +pub struct ProtectedDeviceResponseModel { + #[serde(rename = "object", skip_serializing_if = "Option::is_none")] + pub object: Option, + #[serde(rename = "id", skip_serializing_if = "Option::is_none")] + pub id: Option, + #[serde(rename = "name", skip_serializing_if = "Option::is_none")] + pub name: Option, + #[serde(rename = "type", skip_serializing_if = "Option::is_none")] + pub r#type: Option, + #[serde(rename = "identifier", skip_serializing_if = "Option::is_none")] + pub identifier: Option, + #[serde(rename = "creationDate", skip_serializing_if = "Option::is_none")] + pub creation_date: Option, + #[serde(rename = "encryptedUserKey", skip_serializing_if = "Option::is_none")] + pub encrypted_user_key: Option, + #[serde(rename = "encryptedPublicKey", skip_serializing_if = "Option::is_none")] + pub encrypted_public_key: Option, +} + +impl ProtectedDeviceResponseModel { + pub fn new() -> ProtectedDeviceResponseModel { + ProtectedDeviceResponseModel { + object: None, + id: None, + name: None, + r#type: None, + identifier: None, + creation_date: None, + encrypted_user_key: None, + encrypted_public_key: None, + } + } +} diff --git a/crates/bitwarden-api-api/src/models/secrets_manager_subscribe_request_model.rs b/crates/bitwarden-api-api/src/models/secrets_manager_subscribe_request_model.rs new file mode 100644 index 000000000..2510d8f2c --- /dev/null +++ b/crates/bitwarden-api-api/src/models/secrets_manager_subscribe_request_model.rs @@ -0,0 +1,29 @@ +/* + * Bitwarden Internal API + * + * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator) + * + * The version of the OpenAPI document: latest + * + * Generated by: https://openapi-generator.tech + */ + +#[derive(Clone, Debug, PartialEq, Default, Serialize, Deserialize)] +pub struct SecretsManagerSubscribeRequestModel { + #[serde(rename = "additionalSmSeats")] + pub additional_sm_seats: i32, + #[serde(rename = "additionalServiceAccounts")] + pub additional_service_accounts: i32, +} + +impl SecretsManagerSubscribeRequestModel { + pub fn new( + additional_sm_seats: i32, + additional_service_accounts: i32, + ) -> SecretsManagerSubscribeRequestModel { + SecretsManagerSubscribeRequestModel { + additional_sm_seats, + additional_service_accounts, + } + } +} diff --git a/crates/bitwarden-api-api/src/models/service_account_secrets_details_response_model.rs b/crates/bitwarden-api-api/src/models/service_account_secrets_details_response_model.rs new file mode 100644 index 000000000..5be4d8bb1 --- /dev/null +++ b/crates/bitwarden-api-api/src/models/service_account_secrets_details_response_model.rs @@ -0,0 +1,41 @@ +/* + * Bitwarden Internal API + * + * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator) + * + * The version of the OpenAPI document: latest + * + * Generated by: https://openapi-generator.tech + */ + +#[derive(Clone, Debug, PartialEq, Default, Serialize, Deserialize)] +pub struct ServiceAccountSecretsDetailsResponseModel { + #[serde(rename = "object", skip_serializing_if = "Option::is_none")] + pub object: Option, + #[serde(rename = "id", skip_serializing_if = "Option::is_none")] + pub id: Option, + #[serde(rename = "organizationId", skip_serializing_if = "Option::is_none")] + pub organization_id: Option, + #[serde(rename = "name", skip_serializing_if = "Option::is_none")] + pub name: Option, + #[serde(rename = "creationDate", skip_serializing_if = "Option::is_none")] + pub creation_date: Option, + #[serde(rename = "revisionDate", skip_serializing_if = "Option::is_none")] + pub revision_date: Option, + #[serde(rename = "accessToSecrets", skip_serializing_if = "Option::is_none")] + pub access_to_secrets: Option, +} + +impl ServiceAccountSecretsDetailsResponseModel { + pub fn new() -> ServiceAccountSecretsDetailsResponseModel { + ServiceAccountSecretsDetailsResponseModel { + object: None, + id: None, + organization_id: None, + name: None, + creation_date: None, + revision_date: None, + access_to_secrets: None, + } + } +} diff --git a/crates/bitwarden-api-api/src/models/service_account_secrets_details_response_model_list_response_model.rs b/crates/bitwarden-api-api/src/models/service_account_secrets_details_response_model_list_response_model.rs new file mode 100644 index 000000000..5edfbefba --- /dev/null +++ b/crates/bitwarden-api-api/src/models/service_account_secrets_details_response_model_list_response_model.rs @@ -0,0 +1,29 @@ +/* + * Bitwarden Internal API + * + * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator) + * + * The version of the OpenAPI document: latest + * + * Generated by: https://openapi-generator.tech + */ + +#[derive(Clone, Debug, PartialEq, Default, Serialize, Deserialize)] +pub struct ServiceAccountSecretsDetailsResponseModelListResponseModel { + #[serde(rename = "object", skip_serializing_if = "Option::is_none")] + pub object: Option, + #[serde(rename = "data", skip_serializing_if = "Option::is_none")] + pub data: Option>, + #[serde(rename = "continuationToken", skip_serializing_if = "Option::is_none")] + pub continuation_token: Option, +} + +impl ServiceAccountSecretsDetailsResponseModelListResponseModel { + pub fn new() -> ServiceAccountSecretsDetailsResponseModelListResponseModel { + ServiceAccountSecretsDetailsResponseModelListResponseModel { + object: None, + data: None, + continuation_token: None, + } + } +} diff --git a/crates/bitwarden-api-api/src/models/subscription_response_model.rs b/crates/bitwarden-api-api/src/models/subscription_response_model.rs index 2ebba2a44..c21cbdbea 100644 --- a/crates/bitwarden-api-api/src/models/subscription_response_model.rs +++ b/crates/bitwarden-api-api/src/models/subscription_response_model.rs @@ -22,6 +22,8 @@ pub struct SubscriptionResponseModel { pub upcoming_invoice: Option>, #[serde(rename = "subscription", skip_serializing_if = "Option::is_none")] pub subscription: Option>, + #[serde(rename = "discount", skip_serializing_if = "Option::is_none")] + pub discount: Option>, #[serde(rename = "license", skip_serializing_if = "Option::is_none")] pub license: Option>, #[serde(rename = "expiration", skip_serializing_if = "Option::is_none")] @@ -39,6 +41,7 @@ impl SubscriptionResponseModel { max_storage_gb: None, upcoming_invoice: None, subscription: None, + discount: None, license: None, expiration: None, using_in_app_purchase: None, diff --git a/crates/bitwarden-api-api/src/models/update_devices_trust_request_model.rs b/crates/bitwarden-api-api/src/models/update_devices_trust_request_model.rs new file mode 100644 index 000000000..e4d381d51 --- /dev/null +++ b/crates/bitwarden-api-api/src/models/update_devices_trust_request_model.rs @@ -0,0 +1,43 @@ +/* + * Bitwarden Internal API + * + * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator) + * + * The version of the OpenAPI document: latest + * + * Generated by: https://openapi-generator.tech + */ + +#[derive(Clone, Debug, PartialEq, Default, Serialize, Deserialize)] +pub struct UpdateDevicesTrustRequestModel { + #[serde(rename = "masterPasswordHash", skip_serializing_if = "Option::is_none")] + pub master_password_hash: Option, + #[serde(rename = "otp", skip_serializing_if = "Option::is_none")] + pub otp: Option, + #[serde( + rename = "authRequestAccessCode", + skip_serializing_if = "Option::is_none" + )] + pub auth_request_access_code: Option, + #[serde(rename = "secret", skip_serializing_if = "Option::is_none")] + pub secret: Option, + #[serde(rename = "currentDevice")] + pub current_device: Box, + #[serde(rename = "otherDevices", skip_serializing_if = "Option::is_none")] + pub other_devices: Option>, +} + +impl UpdateDevicesTrustRequestModel { + pub fn new( + current_device: crate::models::DeviceKeysUpdateRequestModel, + ) -> UpdateDevicesTrustRequestModel { + UpdateDevicesTrustRequestModel { + master_password_hash: None, + otp: None, + auth_request_access_code: None, + secret: None, + current_device: Box::new(current_device), + other_devices: None, + } + } +} diff --git a/crates/bitwarden-api-identity/Cargo.toml b/crates/bitwarden-api-identity/Cargo.toml index 70be0560b..7207c5f8e 100644 --- a/crates/bitwarden-api-identity/Cargo.toml +++ b/crates/bitwarden-api-identity/Cargo.toml @@ -13,14 +13,14 @@ categories = ["api-bindings"] edition = "2018" [dependencies] -serde = "^1.0.163" -serde_derive = "^1.0.163" -serde_json = "^1.0.96" -serde_repr = "^0.1.12" -url = "^2.3.1" -uuid = { version = "^1.3.3", features = ["serde"] } +serde = ">=1.0.163, <2" +serde_derive = ">=1.0.163, <2" +serde_json = ">=1.0.96, <2" +serde_repr = ">=0.1.12, <0.2" +url = ">=2.3.1, <3" +uuid = { version = ">=1.3.3, <2", features = ["serde"] } [dependencies.reqwest] -version = "^0.11.18" +version = ">=0.11.18, <0.12" features = ["json", "multipart"] [dev-dependencies] diff --git a/crates/bitwarden-c/Cargo.toml b/crates/bitwarden-c/Cargo.toml index 9181bedae..20f3f5229 100644 --- a/crates/bitwarden-c/Cargo.toml +++ b/crates/bitwarden-c/Cargo.toml @@ -4,7 +4,6 @@ version = "0.1.0" edition = "2021" rust-version = "1.57" -# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [lib] crate-type = ["lib", "staticlib", "cdylib"] bench = false diff --git a/crates/bitwarden-cli/Cargo.toml b/crates/bitwarden-cli/Cargo.toml index 606e24856..0966f7885 100644 --- a/crates/bitwarden-cli/Cargo.toml +++ b/crates/bitwarden-cli/Cargo.toml @@ -5,7 +5,7 @@ edition = "2021" rust-version = "1.57" [dependencies] -clap = { version = "4.3.0", features = ["derive"] } +clap = { version = "4.4.8", features = ["derive"] } color-eyre = "0.6" inquire = "0.6.2" -supports-color = "2.0.0" +supports-color = "2.1.0" diff --git a/crates/bitwarden-json/Cargo.toml b/crates/bitwarden-json/Cargo.toml index 579a8bdab..f7c0fd58d 100644 --- a/crates/bitwarden-json/Cargo.toml +++ b/crates/bitwarden-json/Cargo.toml @@ -18,9 +18,9 @@ internal = ["bitwarden/internal"] # Internal testing methods secrets = ["bitwarden/secrets"] # Secrets manager API [dependencies] +log = ">=0.4.18, <0.5" schemars = ">=0.8.12, <0.9" serde = { version = ">=1.0, <2.0", features = ["derive"] } serde_json = ">=1.0.96, <2.0" -log = ">=0.4.18, <0.5" bitwarden = { path = "../bitwarden" } diff --git a/crates/bitwarden-json/src/client.rs b/crates/bitwarden-json/src/client.rs index f771a5ce4..416067b66 100644 --- a/crates/bitwarden-json/src/client.rs +++ b/crates/bitwarden-json/src/client.rs @@ -46,13 +46,15 @@ impl Client { match cmd { #[cfg(feature = "internal")] - Command::PasswordLogin(req) => self.0.password_login(&req).await.into_string(), + Command::PasswordLogin(req) => self.0.auth().login_password(&req).await.into_string(), #[cfg(feature = "secrets")] - Command::AccessTokenLogin(req) => self.0.access_token_login(&req).await.into_string(), + Command::AccessTokenLogin(req) => { + self.0.auth().login_access_token(&req).await.into_string() + } #[cfg(feature = "internal")] Command::GetUserApiKey(req) => self.0.get_user_api_key(&req).await.into_string(), #[cfg(feature = "internal")] - Command::ApiKeyLogin(req) => self.0.api_key_login(&req).await.into_string(), + Command::ApiKeyLogin(req) => self.0.auth().login_api_key(&req).await.into_string(), #[cfg(feature = "internal")] Command::Sync(req) => self.0.sync(&req).await.into_string(), #[cfg(feature = "internal")] diff --git a/crates/bitwarden-napi/Cargo.toml b/crates/bitwarden-napi/Cargo.toml index 505b68e57..63eae2b8b 100644 --- a/crates/bitwarden-napi/Cargo.toml +++ b/crates/bitwarden-napi/Cargo.toml @@ -16,15 +16,15 @@ rust-version = "1.57" crate-type = ["cdylib", "rlib"] [dependencies] -napi = {version="2", features=["async"]} +env_logger = "0.10.1" +log = "0.4.20" +napi = { version = "2", features = ["async"] } napi-derive = "2" -log = "0.4.18" -env_logger="0.10.0" bitwarden-json = { path = "../bitwarden-json", version = "0.3.0", features = ["secrets"] } [build-dependencies] -napi-build = "2.0.1" +napi-build = "2.1.0" [profile.release] lto = true diff --git a/crates/bitwarden-napi/package-lock.json b/crates/bitwarden-napi/package-lock.json index 4a85cb304..3ccf3a7cb 100644 --- a/crates/bitwarden-napi/package-lock.json +++ b/crates/bitwarden-napi/package-lock.json @@ -55,9 +55,9 @@ } }, "node_modules/@napi-rs/cli": { - "version": "2.16.3", - "resolved": "https://registry.npmjs.org/@napi-rs/cli/-/cli-2.16.3.tgz", - "integrity": "sha512-3mLNPlbbOhpbIUKicLrJtIearlHXUuXL3UeueYyRRplpVMNkdn8xCyzY6PcYZi3JXR8bmCOiWgkVmLnrSL7DKw==", + "version": "2.16.5", + "resolved": "https://registry.npmjs.org/@napi-rs/cli/-/cli-2.16.5.tgz", + "integrity": "sha512-mFEzwrg4IOLngGd2/P6yeqIWgwQNn59Z08n1rndu6kLDq1gg954NH9cM1O9Da0RJuybt46p43lqgSsnAY2mxqA==", "dev": true, "bin": { "napi": "scripts/index.js" @@ -95,9 +95,9 @@ "dev": true }, "node_modules/@types/node": { - "version": "20.8.9", - "resolved": "https://registry.npmjs.org/@types/node/-/node-20.8.9.tgz", - "integrity": "sha512-UzykFsT3FhHb1h7yD4CA4YhBHq545JC0YnEz41xkipN88eKQtL6rSgocL5tbAP6Ola9Izm/Aw4Ora8He4x0BHg==", + "version": "20.9.2", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.9.2.tgz", + "integrity": "sha512-WHZXKFCEyIUJzAwh3NyyTHYSR35SevJ6mZ1nWwJafKtiQbqRTIKSRcw3Ma3acqgsent3RRDqeVwpHntMk+9irg==", "dev": true, "peer": true, "dependencies": { diff --git a/crates/bitwarden-napi/src/client.rs b/crates/bitwarden-napi/src/client.rs index 840fd2533..d794b18f4 100644 --- a/crates/bitwarden-napi/src/client.rs +++ b/crates/bitwarden-napi/src/client.rs @@ -1,7 +1,6 @@ extern crate log; use bitwarden_json::client::Client as JsonClient; -use napi::bindgen_prelude::*; use napi_derive::napi; #[napi] diff --git a/crates/bitwarden-py/Cargo.toml b/crates/bitwarden-py/Cargo.toml index 5d8d95575..94e9e6a43 100644 --- a/crates/bitwarden-py/Cargo.toml +++ b/crates/bitwarden-py/Cargo.toml @@ -4,23 +4,22 @@ version = "0.1.0" edition = "2021" rust-version = "1.57" -# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [lib] name = "bitwarden_py" crate-type = ["cdylib"] [dependencies] -pyo3 = { version = "0.19.1", features = ["extension-module"] } -pyo3-log = "0.8.3" +pyo3 = { version = "0.20.0", features = ["extension-module"] } +pyo3-log = "0.9.0" bitwarden-json = { path = "../bitwarden-json", features = ["secrets"] } [build-dependencies] -pyo3-build-config = { version = "0.19.1" } +pyo3-build-config = { version = "0.20.0" } [target.'cfg(not(target_arch="wasm32"))'.dependencies] -tokio = { version = "1.28.2", features = ["rt-multi-thread", "macros"] } -pyo3-asyncio = { version = "0.19.0", features = [ +tokio = { version = "1.34.0", features = ["rt-multi-thread", "macros"] } +pyo3-asyncio = { version = "0.20.0", features = [ "attributes", "tokio-runtime", ] } diff --git a/crates/bitwarden-uniffi/Cargo.toml b/crates/bitwarden-uniffi/Cargo.toml index 759e84e19..c1a47ea29 100644 --- a/crates/bitwarden-uniffi/Cargo.toml +++ b/crates/bitwarden-uniffi/Cargo.toml @@ -12,19 +12,19 @@ crate-type = ["lib", "staticlib", "cdylib"] bench = false [dependencies] -async-lock = "3.0.0" +async-lock = "3.1.2" chrono = { version = ">=0.4.26, <0.5", features = [ "serde", "std", ], default-features = false } -env_logger = "0.10.0" -uniffi = "=0.24.1" +env_logger = "0.10.1" schemars = { version = ">=0.8, <0.9", optional = true } +uniffi = "=0.25.2" bitwarden = { path = "../bitwarden", features = ["mobile", "internal"] } [build-dependencies] -uniffi = { version = "=0.24.1", features = ["build"] } +uniffi = { version = "=0.25.2", features = ["build"] } [target.'cfg(any(target_os = "android", target_os = "ios"))'.dependencies] openssl = { version = "0.10", features = ["vendored"] } diff --git a/crates/bitwarden-uniffi/README.md b/crates/bitwarden-uniffi/README.md index 7dff0b291..1be7706bb 100644 --- a/crates/bitwarden-uniffi/README.md +++ b/crates/bitwarden-uniffi/README.md @@ -5,6 +5,7 @@ ```bash cargo +nightly rustdoc -p bitwarden -- -Zunstable-options --output-format json cargo +nightly rustdoc -p bitwarden-uniffi -- -Zunstable-options --output-format json +npm run schemas npx ts-node ./support/docs/docs.ts > languages/kotlin/doc.md ``` diff --git a/crates/bitwarden-uniffi/src/crypto.rs b/crates/bitwarden-uniffi/src/crypto.rs new file mode 100644 index 000000000..804202e2f --- /dev/null +++ b/crates/bitwarden-uniffi/src/crypto.rs @@ -0,0 +1,48 @@ +use std::sync::Arc; + +use bitwarden::mobile::crypto::{InitOrgCryptoRequest, InitUserCryptoRequest}; + +use crate::{error::Result, Client}; + +#[derive(uniffi::Object)] +pub struct ClientCrypto(pub(crate) Arc); + +#[uniffi::export] +impl ClientCrypto { + /// Initialization method for the user crypto. Needs to be called before any other crypto operations. + pub async fn initialize_user_crypto(&self, req: InitUserCryptoRequest) -> Result<()> { + Ok(self + .0 + .0 + .write() + .await + .crypto() + .initialize_user_crypto(req) + .await?) + } + + /// Initialization method for the organization crypto. Needs to be called after `initialize_user_crypto` but before any other crypto operations. + pub async fn initialize_org_crypto(&self, req: InitOrgCryptoRequest) -> Result<()> { + Ok(self + .0 + .0 + .write() + .await + .crypto() + .initialize_org_crypto(req) + .await?) + } + + /// Get the uses's decrypted encryption key. Note: It's very important + /// to keep this key safe, as it can be used to decrypt all of the user's data + pub async fn get_user_encryption_key(&self) -> Result { + Ok(self + .0 + .0 + .write() + .await + .crypto() + .get_user_encryption_key() + .await?) + } +} diff --git a/crates/bitwarden-uniffi/src/docs.rs b/crates/bitwarden-uniffi/src/docs.rs index bc47a19c3..d66c78002 100644 --- a/crates/bitwarden-uniffi/src/docs.rs +++ b/crates/bitwarden-uniffi/src/docs.rs @@ -1,7 +1,8 @@ use bitwarden::{ auth::password::MasterPasswordPolicyOptions, client::kdf::Kdf, - mobile::crypto::InitCryptoRequest, + mobile::crypto::{InitOrgCryptoRequest, InitUserCryptoRequest}, + platform::FingerprintRequest, tool::{ExportFormat, PassphraseGeneratorRequest, PasswordGeneratorRequest}, vault::{ Cipher, CipherView, Collection, Folder, FolderView, Send, SendListView, SendView, @@ -24,7 +25,8 @@ pub enum DocRef { SendListView(SendListView), // Crypto - InitCryptoRequest(InitCryptoRequest), + InitUserCryptoRequest(InitUserCryptoRequest), + InitOrgCryptoRequest(InitOrgCryptoRequest), // Generators PasswordGeneratorRequest(PasswordGeneratorRequest), @@ -33,6 +35,9 @@ pub enum DocRef { // Exporters ExportFormat(ExportFormat), + // Platform + FingerprintRequest(FingerprintRequest), + // Auth MasterPasswordPolicyOptions(MasterPasswordPolicyOptions), diff --git a/crates/bitwarden-uniffi/src/error.rs b/crates/bitwarden-uniffi/src/error.rs index 1a1e9d29a..b6175eb3b 100644 --- a/crates/bitwarden-uniffi/src/error.rs +++ b/crates/bitwarden-uniffi/src/error.rs @@ -1,7 +1,7 @@ use std::fmt::{Display, Formatter}; // Name is converted from *Error to *Exception, so we can't just name the enum Error because Exception already exists -#[derive(uniffi::Error)] +#[derive(uniffi::Error, Debug)] #[uniffi(flat_error)] pub enum BitwardenError { E(bitwarden::error::Error), @@ -21,4 +21,12 @@ impl Display for BitwardenError { } } +impl std::error::Error for BitwardenError { + fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { + match self { + BitwardenError::E(e) => Some(e), + } + } +} + pub type Result = std::result::Result; diff --git a/crates/bitwarden-uniffi/src/lib.rs b/crates/bitwarden-uniffi/src/lib.rs index 6e5415d38..13253d9a6 100644 --- a/crates/bitwarden-uniffi/src/lib.rs +++ b/crates/bitwarden-uniffi/src/lib.rs @@ -4,10 +4,12 @@ use std::sync::Arc; use async_lock::RwLock; use auth::ClientAuth; -use bitwarden::{client::client_settings::ClientSettings, mobile::crypto::InitCryptoRequest}; +use bitwarden::client::client_settings::ClientSettings; pub mod auth; +pub mod crypto; mod error; +pub mod platform; pub mod tool; mod uniffi_support; pub mod vault; @@ -15,16 +17,15 @@ pub mod vault; #[cfg(feature = "docs")] pub mod docs; +use crypto::ClientCrypto; use error::Result; +use platform::ClientPlatform; use tool::ClientGenerators; use vault::ClientVault; #[derive(uniffi::Object)] pub struct Client(RwLock); -#[derive(uniffi::Object)] -pub struct ClientCrypto(Arc); - #[uniffi::export] impl Client { /// Initialize a new instance of the SDK client @@ -43,6 +44,10 @@ impl Client { Arc::new(ClientVault(self)) } + pub fn platform(self: Arc) -> Arc { + Arc::new(ClientPlatform(self)) + } + /// Generator operations pub fn generators(self: Arc) -> Arc { Arc::new(ClientGenerators(self)) @@ -58,18 +63,3 @@ impl Client { msg } } - -#[uniffi::export] -impl ClientCrypto { - /// Initialization method for the crypto. Needs to be called before any other crypto operations. - pub async fn initialize_crypto(&self, req: InitCryptoRequest) -> Result<()> { - Ok(self - .0 - .0 - .write() - .await - .crypto() - .initialize_crypto(req) - .await?) - } -} diff --git a/crates/bitwarden-uniffi/src/platform/mod.rs b/crates/bitwarden-uniffi/src/platform/mod.rs new file mode 100644 index 000000000..f4eaaa095 --- /dev/null +++ b/crates/bitwarden-uniffi/src/platform/mod.rs @@ -0,0 +1,16 @@ +use std::sync::Arc; + +use bitwarden::platform::FingerprintRequest; + +use crate::{error::Result, Client}; + +#[derive(uniffi::Object)] +pub struct ClientPlatform(pub(crate) Arc); + +#[uniffi::export] +impl ClientPlatform { + /// Fingerprint + pub async fn fingerprint(&self, req: FingerprintRequest) -> Result { + Ok(self.0 .0.read().await.fingerprint(&req)?.fingerprint) + } +} diff --git a/crates/bitwarden-uniffi/src/sdk.udl b/crates/bitwarden-uniffi/src/sdk.udl deleted file mode 100644 index 96cfb31c0..000000000 --- a/crates/bitwarden-uniffi/src/sdk.udl +++ /dev/null @@ -1,8 +0,0 @@ -interface Client { - constructor(optional string settings = ""); - string run_command(string command); -}; - -namespace bitwarden { - -}; diff --git a/crates/bitwarden-uniffi/src/uniffi_support.rs b/crates/bitwarden-uniffi/src/uniffi_support.rs index f2a0bfde6..82cd343a7 100644 --- a/crates/bitwarden-uniffi/src/uniffi_support.rs +++ b/crates/bitwarden-uniffi/src/uniffi_support.rs @@ -1,16 +1,3 @@ -use crate::UniffiCustomTypeConverter; - +// Forward the type definitions to the main bitwarden crate type DateTime = chrono::DateTime; -uniffi::custom_type!(DateTime, std::time::SystemTime); - -impl UniffiCustomTypeConverter for chrono::DateTime { - type Builtin = std::time::SystemTime; - - fn into_custom(val: Self::Builtin) -> uniffi::Result { - Ok(Self::from(val)) - } - - fn from_custom(obj: Self) -> Self::Builtin { - obj.into() - } -} +uniffi::ffi_converter_forward!(DateTime, bitwarden::UniFfiTag, crate::UniFfiTag); diff --git a/crates/bitwarden-uniffi/uniffi.toml b/crates/bitwarden-uniffi/uniffi.toml index 456a3676e..c9cb0899c 100644 --- a/crates/bitwarden-uniffi/uniffi.toml +++ b/crates/bitwarden-uniffi/uniffi.toml @@ -1,7 +1,9 @@ [bindings.kotlin] package_name = "com.bitwarden.sdk" cdylib_name = "bitwarden_uniffi" +generate_immutable_records = true [bindings.swift] ffi_module_name = "BitwardenFFI" module_name = "BitwardenSDK" +generate_immutable_records = true diff --git a/crates/bitwarden-wasm/Cargo.toml b/crates/bitwarden-wasm/Cargo.toml index 6055388fb..7ad0d1c3e 100644 --- a/crates/bitwarden-wasm/Cargo.toml +++ b/crates/bitwarden-wasm/Cargo.toml @@ -4,18 +4,17 @@ version = "0.1.0" edition = "2021" rust-version = "1.57" -# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [lib] crate-type = ["cdylib", "rlib"] [dependencies] -js-sys = "0.3.63" -serde = {version = "1.0.163", features = ["derive"] } -wasm-bindgen = { version = "0.2.86", features = ["serde-serialize"] } -wasm-bindgen-futures = "0.4.36" console_error_panic_hook = "0.1.7" console_log = { version = "1.0.0", features = ["color"] } -log = "0.4.18" +js-sys = "0.3.63" +log = "0.4.20" +serde = { version = "1.0.193", features = ["derive"] } +wasm-bindgen = { version = "=0.2.87", features = ["serde-serialize"] } +wasm-bindgen-futures = "0.4.36" bitwarden-json = { path = "../bitwarden-json", features = ["secrets"] } diff --git a/crates/bitwarden/CHANGELOG.md b/crates/bitwarden/CHANGELOG.md index 5569ebd7b..8bb18ff72 100644 --- a/crates/bitwarden/CHANGELOG.md +++ b/crates/bitwarden/CHANGELOG.md @@ -7,6 +7,11 @@ adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). ## [Unreleased] +### Deprecated + +- `client.access_token_login()` is now deprecated and will be removed in a future release. Please + use `client.auth().login_access_token()` instead. (#319) + ## [0.3.1] - 2023-10-13 ### Changed diff --git a/crates/bitwarden/Cargo.toml b/crates/bitwarden/Cargo.toml index 0090ebb0e..aea2cfe9e 100644 --- a/crates/bitwarden/Cargo.toml +++ b/crates/bitwarden/Cargo.toml @@ -20,44 +20,44 @@ internal = [] # Internal testing methods mobile = ["uniffi", "internal"] # Mobile-specific features [dependencies] +aes = ">=0.8.2, <0.9" +argon2 = { version = ">=0.5.0, <0.6", features = [ + "alloc", +], default-features = false } +assert_matches = ">=1.5.0, <2.0" base64 = ">=0.21.2, <0.22" +bitwarden-api-api = { path = "../bitwarden-api-api", version = "=0.2.2" } +bitwarden-api-identity = { path = "../bitwarden-api-identity", version = "=0.2.2" } +cbc = { version = ">=0.1.2, <0.2", features = ["alloc"] } +chrono = { version = ">=0.4.26, <0.5", features = [ + "serde", + "std", +], default-features = false } +# We don't use this directly (it's used by rand), but we need it here to enable WASM support +getrandom = { version = ">=0.2.9, <0.3", features = ["js"] } +hkdf = ">=0.12.3, <0.13" +hmac = ">=0.12.1, <0.13" lazy_static = ">=1.4.0, <2.0" +log = ">=0.4.18, <0.5" +num-bigint = ">=0.4, <0.5" +num-traits = ">=0.2.15, <0.3" +pbkdf2 = { version = ">=0.12.1, <0.13", default-features = false } +rand = ">=0.8.5, <0.9" reqwest = { version = ">=0.11, <0.12", features = ["json"] } +rsa = ">=0.9.2, <0.10" +schemars = { version = ">=0.8, <0.9", features = ["uuid1", "chrono"] } serde = { version = ">=1.0, <2.0", features = ["derive"] } serde_json = ">=1.0.96, <2.0" serde_qs = ">=0.12.0, <0.13" serde_repr = ">=0.1.12, <0.2" -schemars = { version = ">=0.8, <0.9", features = ["uuid1", "chrono"] } -log = ">=0.4.18, <0.5" -assert_matches = ">=1.5.0, <2.0" -thiserror = ">=1.0.40, <2.0" -aes = ">=0.8.2, <0.9" -cbc = { version = ">=0.1.2, <0.2", features = ["alloc"] } -hkdf = ">=0.12.3, <0.13" -hmac = ">=0.12.1, <0.13" -rsa = ">=0.9.2, <0.10" sha1 = ">=0.10.5, <0.11" sha2 = ">=0.10.6, <0.11" -pbkdf2 = { version = ">=0.12.1, <0.13", default-features = false } -argon2 = { version = ">=0.5.0, <0.6", features = [ - "alloc", -], default-features = false } -rand = ">=0.8.5, <0.9" -num-bigint = ">=0.4, <0.5" -num-traits = ">=0.2.15, <0.3" +subtle = ">=2.5.0, <3.0" +thiserror = ">=1.0.40, <2.0" +uniffi = { version = "=0.25.2", optional = true } uuid = { version = ">=1.3.3, <2.0", features = ["serde"] } -chrono = { version = ">=0.4.26, <0.5", features = [ - "serde", - "std", -], default-features = false } -uniffi = { version = "=0.24.1", optional = true } - -# We don't use this directly (it's used by rand), but we need it here to enable WASM support -getrandom = { version = ">=0.2.9", features = ["js"] } - -bitwarden-api-identity = { path = "../bitwarden-api-identity", version = "=0.2.2" } -bitwarden-api-api = { path = "../bitwarden-api-api", version = "=0.2.2" } [dev-dependencies] -tokio = { version = "1.28.2", features = ["rt", "macros"] } -wiremock = "0.5.18" +rand_chacha = "0.3.1" +tokio = { version = "1.34.0", features = ["rt", "macros"] } +wiremock = "0.5.21" diff --git a/crates/bitwarden/README.md b/crates/bitwarden/README.md index a0708b7e6..67c347583 100644 --- a/crates/bitwarden/README.md +++ b/crates/bitwarden/README.md @@ -42,7 +42,7 @@ async fn test() -> Result<()> { // Before we operate, we need to authenticate with a token let token = AccessTokenLoginRequest { access_token: String::from("") }; - client.access_token_login(&token).await.unwrap(); + client.auth().login_access_token(&token).await.unwrap(); let org_id = SecretIdentifiersRequest { organization_id: Uuid::parse_str("00000000-0000-0000-0000-000000000000").unwrap() }; println!("Stored secrets: {:#?}", client.secrets().list(&org_id).await.unwrap()); diff --git a/crates/bitwarden/src/auth/api/request/mod.rs b/crates/bitwarden/src/auth/api/request/mod.rs index 1277fa969..85b36a983 100644 --- a/crates/bitwarden/src/auth/api/request/mod.rs +++ b/crates/bitwarden/src/auth/api/request/mod.rs @@ -35,10 +35,10 @@ async fn send_identity_connect_request( &configurations.identity.base_path )) .header( - "Content-Type", + reqwest::header::CONTENT_TYPE, "application/x-www-form-urlencoded; charset=utf-8", ) - .header("Accept", "application/json") + .header(reqwest::header::ACCEPT, "application/json") .header("Device-Type", configurations.device_type as usize); if let Some(ref user_agent) = configurations.identity.user_agent { diff --git a/crates/bitwarden/src/auth/client_auth.rs b/crates/bitwarden/src/auth/client_auth.rs index 8ffca0a44..aa1615c3b 100644 --- a/crates/bitwarden/src/auth/client_auth.rs +++ b/crates/bitwarden/src/auth/client_auth.rs @@ -1,14 +1,40 @@ -use super::{ - password::{password_strength, satisfies_policy, MasterPasswordPolicyOptions}, - register::{make_register_keys, register}, - RegisterKeyResponse, RegisterRequest, +#[cfg(feature = "secrets")] +use crate::auth::login::{login_access_token, AccessTokenLoginRequest, AccessTokenLoginResponse}; +use crate::{auth::renew::renew_token, error::Result, Client}; +#[cfg(feature = "internal")] +use crate::{ + auth::{ + login::{ + login_api_key, login_password, send_two_factor_email, ApiKeyLoginRequest, + ApiKeyLoginResponse, PasswordLoginRequest, PasswordLoginResponse, + TwoFactorEmailRequest, + }, + password::{password_strength, satisfies_policy, MasterPasswordPolicyOptions}, + register::{make_register_keys, register}, + RegisterKeyResponse, RegisterRequest, + }, + client::kdf::Kdf, }; -use crate::{client::kdf::Kdf, error::Result, Client}; pub struct ClientAuth<'a> { pub(crate) client: &'a mut crate::Client, } +impl<'a> ClientAuth<'a> { + pub async fn renew_token(&mut self) -> Result<()> { + renew_token(self.client).await + } + + #[cfg(feature = "secrets")] + pub async fn login_access_token( + &mut self, + input: &AccessTokenLoginRequest, + ) -> Result { + login_access_token(self.client, input).await + } +} + +#[cfg(feature = "internal")] impl<'a> ClientAuth<'a> { pub async fn password_strength( &self, @@ -37,10 +63,33 @@ impl<'a> ClientAuth<'a> { make_register_keys(email, password, kdf) } - #[cfg(feature = "internal")] pub async fn register(&mut self, input: &RegisterRequest) -> Result<()> { register(self.client, input).await } + + pub async fn prelogin(&mut self, email: String) -> Result { + use crate::auth::login::request_prelogin; + + request_prelogin(self.client, email).await?.try_into() + } + + pub async fn login_password( + &mut self, + input: &PasswordLoginRequest, + ) -> Result { + login_password(self.client, input).await + } + + pub async fn login_api_key( + &mut self, + input: &ApiKeyLoginRequest, + ) -> Result { + login_api_key(self.client, input).await + } + + pub async fn send_two_factor_email(&mut self, tf: &TwoFactorEmailRequest) -> Result<()> { + send_two_factor_email(self.client, tf).await + } } impl<'a> Client { @@ -48,3 +97,101 @@ impl<'a> Client { ClientAuth { client: self } } } + +#[cfg(test)] +mod tests { + + #[cfg(feature = "secrets")] + #[tokio::test] + async fn test_access_token_login() { + use wiremock::{matchers, Mock, ResponseTemplate}; + + use crate::{auth::login::AccessTokenLoginRequest, secrets_manager::secrets::*}; + + // Create the mock server with the necessary routes for this test + let (_server, mut client) = crate::util::start_mock(vec![ + Mock::given(matchers::path("/identity/connect/token")) + .respond_with(ResponseTemplate::new(200).set_body_json( + serde_json::json!({ + "access_token":"eyJhbGciOiJSUzI1NiIsImtpZCI6IjMwMURENkE1MEU4NEUxRDA5MUM4MUQzQjAwQkY5MDEwQzg1REJEOUFSUzI1NiIsInR5cCI6\ + ImF0K2p3dCIsIng1dCI6Ik1CM1dwUTZFNGRDUnlCMDdBTC1RRU1oZHZabyJ9.eyJuYmYiOjE2NzUxMDM3ODEsImV4cCI6MTY3NTEwNzM4MSwiaXNzIjo\ + iaHR0cDovL2xvY2FsaG9zdCIsImNsaWVudF9pZCI6ImVjMmMxZDQ2LTZhNGItNDc1MS1hMzEwLWFmOTYwMTMxN2YyZCIsInN1YiI6ImQzNDgwNGNhLTR\ + mNmMtNDM5Mi04NmI3LWFmOTYwMTMxNzVkMCIsIm9yZ2FuaXphdGlvbiI6ImY0ZTQ0YTdmLTExOTAtNDMyYS05ZDRhLWFmOTYwMTMxMjdjYiIsImp0aSI\ + 6IjU3QUU0NzQ0MzIwNzk1RThGQkQ4MUIxNDA2RDQyNTQyIiwiaWF0IjoxNjc1MTAzNzgxLCJzY29wZSI6WyJhcGkuc2VjcmV0cyJdfQ.GRKYzqgJZHEE\ + ZHsJkhVZH8zjYhY3hUvM4rhdV3FU10WlCteZdKHrPIadCUh-Oz9DxIAA2HfALLhj1chL4JgwPmZgPcVS2G8gk8XeBmZXowpVWJ11TXS1gYrM9syXbv9j\ + 0JUCdpeshH7e56WnlpVynyUwIum9hmYGZ_XJUfmGtlKLuNjYnawTwLEeR005uEjxq3qI1kti-WFnw8ciL4a6HLNulgiFw1dAvs4c7J0souShMfrnFO3g\ + SOHff5kKD3hBB9ynDBnJQSFYJ7dFWHIjhqs0Vj-9h0yXXCcHvu7dVGpaiNjNPxbh6YeXnY6UWcmHLDtFYsG2BWcNvVD4-VgGxXt3cMhrn7l3fSYuo32Z\ + Yk4Wop73XuxqF2fmfmBdZqGI1BafhENCcZw_bpPSfK2uHipfztrgYnrzwvzedz0rjFKbhDyrjzuRauX5dqVJ4ntPeT9g_I5n71gLxiP7eClyAx5RxdF6\ + He87NwC8i-hLBhugIvLTiDj-Sk9HvMth6zaD0ebxd56wDjq8-CMG_WcgusDqNzKFHqWNDHBXt8MLeTgZAR2rQMIMFZqFgsJlRflbig8YewmNUA9wAU74\ + TfxLY1foO7Xpg49vceB7C-PlvGi1VtX6F2i0tc_67lA5kWXnnKBPBUyspoIrmAUCwfms5nTTqA9xXAojMhRHAos_OdM", + "expires_in":3600, + "token_type":"Bearer", + "scope":"api.secrets", + "encrypted_payload":"2.E9fE8+M/VWMfhhim1KlCbQ==|eLsHR484S/tJbIkM6spnG/HP65tj9A6Tba7kAAvUp+rYuQmGLixiOCfMsqt5OvBctDfvvr/Aes\ + Bu7cZimPLyOEhqEAjn52jF0eaI38XZfeOG2VJl0LOf60Wkfh3ryAMvfvLj3G4ZCNYU8sNgoC2+IQ==|lNApuCQ4Pyakfo/wwuuajWNaEX/2MW8/3rjXB/V7n+k="}) + )), + Mock::given(matchers::path("/api/organizations/f4e44a7f-1190-432a-9d4a-af96013127cb/secrets")) + .respond_with(ResponseTemplate::new(200).set_body_json( + serde_json::json!({ + "secrets":[{ + "id":"15744a66-341a-4c62-af50-af960166b6bc", + "organizationId":"f4e44a7f-1190-432a-9d4a-af96013127cb", + "key":"2.pMS6/icTQABtulw52pq2lg==|XXbxKxDTh+mWiN1HjH2N1w==|Q6PkuT+KX/axrgN9ubD5Ajk2YNwxQkgs3WJM0S0wtG8=", + "creationDate":"2023-01-26T21:46:02.2182556Z", + "revisionDate":"2023-01-26T21:46:02.2182557Z" + }], + "projects":[], + "object":"SecretsWithProjectsList" + }) + )), + Mock::given(matchers::path("/api/secrets/15744a66-341a-4c62-af50-af960166b6bc")) + .respond_with(ResponseTemplate::new(200).set_body_json( + serde_json::json!({ + "id":"15744a66-341a-4c62-af50-af960166b6bc", + "organizationId":"f4e44a7f-1190-432a-9d4a-af96013127cb", + "key":"2.pMS6/icTQABtulw52pq2lg==|XXbxKxDTh+mWiN1HjH2N1w==|Q6PkuT+KX/axrgN9ubD5Ajk2YNwxQkgs3WJM0S0wtG8=", + "value":"2.Gl34n9JYABC7V21qHcBzHg==|c1Ds244pob7i+8+MXe4++w==|Shimz/qKMYZmzSFWdeBzFb9dFz7oF6Uv9oqkws7rEe0=", + "note":"2.Cn9ABJy7+WfR4uUHwdYepg==|+nbJyU/6hSknoa5dcEJEUg==|1DTp/ZbwGO3L3RN+VMsCHz8XDr8egn/M5iSitGGysPA=", + "creationDate":"2023-01-26T21:46:02.2182556Z", + "revisionDate":"2023-01-26T21:46:02.2182557Z", + "object":"secret" + }) + )) + ]).await; + + // Test the login is correct and we store the returned organization ID correctly + let res = client + .auth() + .login_access_token(&AccessTokenLoginRequest { + access_token: "0.ec2c1d46-6a4b-4751-a310-af9601317f2d.C2IgxjjLF7qSshsbwe8JGcbM075YXw:X8vbvA0bduihIDe/qrzIQQ==".into(), + }) + .await + .unwrap(); + assert!(res.authenticated); + let organization_id = client.get_access_token_organization().unwrap(); + assert_eq!( + organization_id.to_string(), + "f4e44a7f-1190-432a-9d4a-af96013127cb" + ); + + // Test that we can retrieve the list of secrets correctly + let mut res = client + .secrets() + .list(&SecretIdentifiersRequest { organization_id }) + .await + .unwrap(); + assert_eq!(res.data.len(), 1); + + // Test that given a secret ID we can get it's data + let res = client + .secrets() + .get(&SecretGetRequest { + id: res.data.remove(0).id, + }) + .await + .unwrap(); + assert_eq!(res.key, "TEST"); + assert_eq!(res.note, "TEST"); + assert_eq!(res.value, "TEST"); + } +} diff --git a/crates/bitwarden/src/auth/jwt_token.rs b/crates/bitwarden/src/auth/jwt_token.rs new file mode 100644 index 000000000..14cbaf84f --- /dev/null +++ b/crates/bitwarden/src/auth/jwt_token.rs @@ -0,0 +1,71 @@ +use std::str::FromStr; + +use base64::Engine; + +use crate::{error::Result, util::BASE64_ENGINE}; + +/// A Bitwarden secrets manager JWT Token. +/// +/// References: +/// - +/// - +/// +/// TODO: We need to expand this to support user based JWT tokens. +#[derive(serde::Deserialize)] +pub struct JWTToken { + pub sub: String, + pub email: Option, + pub organization: Option, + pub scope: Vec, +} + +impl FromStr for JWTToken { + type Err = crate::error::Error; + + /// Parses a JWT token from a string. + /// + /// **Note:** This function does not validate the token signature. + fn from_str(s: &str) -> Result { + let split = s.split('.').collect::>(); + if split.len() != 3 { + return Err(crate::error::Error::Internal( + "JWT token has an invalid number of parts", + )); + } + let decoded = BASE64_ENGINE.decode(split[1])?; + Ok(serde_json::from_slice(&decoded)?) + } +} + +#[cfg(test)] +mod tests { + use crate::auth::jwt_token::JWTToken; + + #[test] + fn can_decode_jwt() { + let jwt = "eyJhbGciOiJSUzI1NiIsImtpZCI6IjMwMURENkE1MEU4NEUxRDA5MUM4MUQzQjAwQkY5MDEwQz\ + g1REJEOUFSUzI1NiIsInR5cCI6ImF0K2p3dCIsIng1dCI6Ik1CM1dwUTZFNGRDUnlCMDdBTC1RRU1oZHZabyJ9.eyJu\ + YmYiOjE2NzUxMDM1NzcsImV4cCI6MTY3NTEwNzE3NywiaXNzIjoiaHR0cDovL2xvY2FsaG9zdCIsImNsaWVudF9pZCI\ + 6IndlYiIsInN1YiI6ImUyNWQzN2YzLWI2MDMtNDBkZS04NGJhLWFmOTYwMTJmNWE0MiIsImF1dGhfdGltZSI6MTY3NT\ + EwMzU0OSwiaWRwIjoiYml0d2FyZGVuIiwicHJlbWl1bSI6ZmFsc2UsImVtYWlsIjoidGVzdEBiaXR3YXJkZW4uY29tI\ + iwiZW1haWxfdmVyaWZpZWQiOnRydWUsInNzdGFtcCI6IkUzNElDWVhRUFRDS01EVldBREZDNktHNDJCQldJRDdJIiwi\ + bmFtZSI6IlRlc3QiLCJvcmdvd25lciI6ImY0ZTQ0YTdmLTExOTAtNDMyYS05ZDRhLWFmOTYwMTMxMjdjYiIsImRldml\ + jZSI6Ijg5Mjg5M2FiLWRkNDMtNDUwYS04NGI1LWFhOWM1YjdiYjJkOCIsImp0aSI6IkEzMkVFNjY5NDdEQzlDNUE2MT\ + IwRURBRTIwNzc5OUJFIiwiaWF0IjoxNjc1MTAzNTc3LCJzY29wZSI6WyJhcGkiLCJvZmZsaW5lX2FjY2VzcyJdLCJhb\ + XIiOlsiQXBwbGljYXRpb24iXX0.AyDkKvjmyaSPQViQSa2sGTKIkDGrUAtDmwpE57K4DDWT0QvwDe7FMktmwiF4LH36\ + wx_FnpH21VI1pzwJeTHXtaz3niANJtQZjzGFsNAna_95vrsxZC2YizgGlt6mX4YIGmAw9DiYrmaN0BvQOEm_caV_u6f\ + a30iz9Kvjxf7cpzeZvPEysxGpB3k3TRYTkFUdV43HiXdhXMBhyyOpFU6Fk6yA41y7-8bGYc5mYGknWktmPD9Yx-1xKL\ + ftFja1SnCoLPWvDeK60lqWZQiT4tZHCYJ7m0bBNCccYHc2Kk2Bo5-UoyDxazPwsqMxeNfjlaUuj3o5N_uQ-4n_gVbeA\ + qWV2wrel5UhYjWnczMSLBtt9p0W35kkBPt3ZAnRWMtQMPNH04p-_L6cG-Xu6lDksBTwaavcmtnCKG8V91826EiQ8MrF\ + wGWQRZV6tPKTDAYCgSAZGBY3QDmPGT5BeFcg5Ag_nYYIIifKP-kv10v_N-TOcT3NeGBOUlAZ-9m7iT7Rk3vC--SDZdA\ + U5turoBFiiPL2XXfAjM7P0r7J91gfXc0FaD6I2jDxOmym5h7Yn5phLsbC2NlIXkZp54dKHICenPl4ve6ndDIJacVeS5\ + f3LEddAPV8cAFza4DjA8pZJLFrMyRvMXcL_PjKF8qPVzqVWh03lfJ4clOIxR2gOuWIc902Y5E"; + + let token: JWTToken = jwt.parse().unwrap(); + assert_eq!(token.sub, "e25d37f3-b603-40de-84ba-af96012f5a42"); + assert_eq!(token.email.as_deref(), Some("test@bitwarden.com")); + assert_eq!(token.organization.as_deref(), None); + assert_eq!(token.scope[0], "api"); + assert_eq!(token.scope[1], "offline_access"); + } +} diff --git a/crates/bitwarden/src/auth/login/access_token.rs b/crates/bitwarden/src/auth/login/access_token.rs index e7d60951c..1ec8c8b34 100644 --- a/crates/bitwarden/src/auth/login/access_token.rs +++ b/crates/bitwarden/src/auth/login/access_token.rs @@ -6,15 +6,16 @@ use crate::{ auth::{ api::{request::AccessTokenRequest, response::IdentityTokenResponse}, login::{response::two_factor::TwoFactorProviders, PasswordLoginResponse}, + JWTToken, }, client::{AccessToken, LoginMethod, ServiceAccountLoginMethod}, crypto::{EncString, KeyDecryptable, SymmetricCryptoKey}, error::{Error, Result}, - util::{decode_token, BASE64_ENGINE}, + util::BASE64_ENGINE, Client, }; -pub(crate) async fn access_token_login( +pub(crate) async fn login_access_token( client: &mut Client, input: &AccessTokenLoginRequest, ) -> Result { @@ -44,7 +45,7 @@ pub(crate) async fn access_token_login( let encryption_key = SymmetricCryptoKey::try_from(encryption_key.as_slice())?; - let access_token_obj = decode_token(&r.access_token)?; + let access_token_obj: JWTToken = r.access_token.parse()?; // This should always be Some() when logging in with an access token let organization_id = access_token_obj diff --git a/crates/bitwarden/src/auth/login/api_key.rs b/crates/bitwarden/src/auth/login/api_key.rs index cdbc383ba..db5cbe01d 100644 --- a/crates/bitwarden/src/auth/login/api_key.rs +++ b/crates/bitwarden/src/auth/login/api_key.rs @@ -5,15 +5,15 @@ use crate::{ auth::{ api::{request::ApiTokenRequest, response::IdentityTokenResponse}, login::{response::two_factor::TwoFactorProviders, PasswordLoginResponse}, + JWTToken, }, client::{LoginMethod, UserLoginMethod}, crypto::EncString, error::{Error, Result}, - util::decode_token, Client, }; -pub(crate) async fn api_key_login( +pub(crate) async fn login_api_key( client: &mut Client, input: &ApiKeyLoginRequest, ) -> Result { @@ -23,14 +23,14 @@ pub(crate) async fn api_key_login( let response = request_api_identity_tokens(client, input).await?; if let IdentityTokenResponse::Authenticated(r) = &response { - let access_token_obj = decode_token(&r.access_token)?; + let access_token_obj: JWTToken = r.access_token.parse()?; // This should always be Some() when logging in with an api key let email = access_token_obj .email .ok_or(Error::Internal("Access token doesn't contain email"))?; - let kdf = client.prelogin(email.clone()).await?; + let kdf = client.auth().prelogin(email.clone()).await?; client.set_tokens( r.access_token.clone(), diff --git a/crates/bitwarden/src/auth/login/mod.rs b/crates/bitwarden/src/auth/login/mod.rs index 9e1dbb818..4ec9d1b49 100644 --- a/crates/bitwarden/src/auth/login/mod.rs +++ b/crates/bitwarden/src/auth/login/mod.rs @@ -14,7 +14,7 @@ pub mod response; mod password; #[cfg(feature = "internal")] -pub(crate) use password::password_login; +pub(crate) use password::login_password; #[cfg(feature = "internal")] pub use password::PasswordLoginRequest; pub use password::PasswordLoginResponse; @@ -28,14 +28,14 @@ pub use two_factor::{TwoFactorEmailRequest, TwoFactorProvider, TwoFactorRequest} #[cfg(feature = "internal")] mod api_key; #[cfg(feature = "internal")] -pub(crate) use api_key::api_key_login; +pub(crate) use api_key::login_api_key; #[cfg(feature = "internal")] pub use api_key::{ApiKeyLoginRequest, ApiKeyLoginResponse}; #[cfg(feature = "secrets")] mod access_token; #[cfg(feature = "secrets")] -pub(crate) use access_token::access_token_login; +pub(super) use access_token::login_access_token; #[cfg(feature = "secrets")] pub use access_token::{AccessTokenLoginRequest, AccessTokenLoginResponse}; diff --git a/crates/bitwarden/src/auth/login/password.rs b/crates/bitwarden/src/auth/login/password.rs index c86f7019f..a320131ea 100644 --- a/crates/bitwarden/src/auth/login/password.rs +++ b/crates/bitwarden/src/auth/login/password.rs @@ -22,7 +22,7 @@ use crate::{ }; #[cfg(feature = "internal")] -pub(crate) async fn password_login( +pub(crate) async fn login_password( client: &mut Client, input: &PasswordLoginRequest, ) -> Result { diff --git a/crates/bitwarden/src/auth/login/two_factor.rs b/crates/bitwarden/src/auth/login/two_factor.rs index 04c411349..d8ede2473 100644 --- a/crates/bitwarden/src/auth/login/two_factor.rs +++ b/crates/bitwarden/src/auth/login/two_factor.rs @@ -20,7 +20,7 @@ pub(crate) async fn send_two_factor_email( input: &TwoFactorEmailRequest, ) -> Result<()> { // TODO: This should be resolved from the client - let kdf = client.prelogin(input.email.clone()).await?; + let kdf = client.auth().prelogin(input.email.clone()).await?; let password_hash = determine_password_hash(&input.email, &kdf, &input.password).await?; diff --git a/crates/bitwarden/src/auth/mod.rs b/crates/bitwarden/src/auth/mod.rs index f2f6a3144..023f3270b 100644 --- a/crates/bitwarden/src/auth/mod.rs +++ b/crates/bitwarden/src/auth/mod.rs @@ -1,10 +1,11 @@ pub(super) mod api; -#[cfg(feature = "internal")] pub mod client_auth; +mod jwt_token; pub mod login; #[cfg(feature = "internal")] pub mod password; pub mod renew; +pub use jwt_token::JWTToken; #[cfg(feature = "internal")] mod register; diff --git a/crates/bitwarden/src/auth/register.rs b/crates/bitwarden/src/auth/register.rs index 8336ec9c9..206c1905b 100644 --- a/crates/bitwarden/src/auth/register.rs +++ b/crates/bitwarden/src/auth/register.rs @@ -79,7 +79,7 @@ pub(super) fn make_register_keys( #[cfg_attr(feature = "mobile", derive(uniffi::Record))] pub struct RegisterKeyResponse { - master_password_hash: String, - encrypted_user_key: String, - keys: RsaKeyPair, + pub master_password_hash: String, + pub encrypted_user_key: String, + pub keys: RsaKeyPair, } diff --git a/crates/bitwarden/src/client/client.rs b/crates/bitwarden/src/client/client.rs index f6eb0c954..5133a5c01 100644 --- a/crates/bitwarden/src/client/client.rs +++ b/crates/bitwarden/src/client/client.rs @@ -1,11 +1,12 @@ use std::time::{Duration, Instant}; +use reqwest::header::{self}; +use uuid::Uuid; + +#[cfg(feature = "secrets")] +use crate::auth::login::{AccessTokenLoginRequest, AccessTokenLoginResponse}; #[cfg(feature = "internal")] use crate::{ - auth::login::{ - api_key_login, password_login, send_two_factor_email, ApiKeyLoginRequest, - ApiKeyLoginResponse, PasswordLoginRequest, PasswordLoginResponse, TwoFactorEmailRequest, - }, client::kdf::Kdf, crypto::EncString, platform::{ @@ -13,13 +14,7 @@ use crate::{ SecretVerificationRequest, SyncRequest, SyncResponse, UserApiKeyResponse, }, }; -use reqwest::header::{self}; -use uuid::Uuid; - -#[cfg(feature = "secrets")] -use crate::auth::login::{access_token_login, AccessTokenLoginRequest, AccessTokenLoginResponse}; use crate::{ - auth::renew::renew_token, client::{ client_settings::{ClientSettings, DeviceType}, encryption_settings::EncryptionSettings, @@ -133,39 +128,17 @@ impl Client { pub(crate) async fn get_api_configurations(&mut self) -> &ApiConfigurations { // At the moment we ignore the error result from the token renewal, if it fails, // the token will end up expiring and the next operation is going to fail anyway. - self.renew_token().await.ok(); + self.auth().renew_token().await.ok(); &self.__api_configurations } - #[cfg(feature = "internal")] - pub async fn prelogin(&mut self, email: String) -> Result { - use crate::auth::login::request_prelogin; - - request_prelogin(self, email).await?.try_into() - } - - #[cfg(feature = "internal")] - pub async fn password_login( - &mut self, - input: &PasswordLoginRequest, - ) -> Result { - password_login(self, input).await - } - - #[cfg(feature = "internal")] - pub async fn api_key_login( - &mut self, - input: &ApiKeyLoginRequest, - ) -> Result { - api_key_login(self, input).await - } - #[cfg(feature = "secrets")] + #[deprecated(note = "Use auth().login_access_token() instead")] pub async fn access_token_login( &mut self, input: &AccessTokenLoginRequest, ) -> Result { - access_token_login(self, input).await + self.auth().login_access_token(input).await } #[cfg(feature = "internal")] @@ -223,10 +196,6 @@ impl Client { self.__api_configurations.api.oauth_access_token = Some(token); } - pub async fn renew_token(&mut self) -> Result<()> { - renew_token(self).await - } - #[cfg(feature = "internal")] pub fn is_authed(&self) -> bool { self.token.is_some() || self.login_method.is_some() @@ -253,6 +222,23 @@ impl Client { Ok(self.encryption_settings.as_ref().unwrap()) } + #[cfg(feature = "mobile")] + pub(crate) fn initialize_user_crypto_decrypted_key( + &mut self, + decrypted_user_key: &str, + private_key: EncString, + ) -> Result<&EncryptionSettings> { + let user_key = decrypted_user_key.parse::()?; + self.encryption_settings = Some(EncryptionSettings::new_decrypted_key( + user_key, + private_key, + )?); + Ok(self + .encryption_settings + .as_ref() + .expect("It was initialized on the previous line")) + } + pub(crate) fn initialize_crypto_single_key( &mut self, key: SymmetricCryptoKey, @@ -276,107 +262,7 @@ impl Client { } #[cfg(feature = "internal")] - pub fn fingerprint(&mut self, input: &FingerprintRequest) -> Result { + pub fn fingerprint(&self, input: &FingerprintRequest) -> Result { generate_fingerprint(input) } - - #[cfg(feature = "internal")] - pub async fn send_two_factor_email(&mut self, tf: &TwoFactorEmailRequest) -> Result<()> { - send_two_factor_email(self, tf).await - } -} - -#[cfg(test)] -mod tests { - use wiremock::{matchers, Mock, ResponseTemplate}; - - use crate::{auth::login::AccessTokenLoginRequest, secrets_manager::secrets::*}; - - #[tokio::test] - async fn test_access_token_login() { - // Create the mock server with the necessary routes for this test - let (_server, mut client) = crate::util::start_mock(vec![ - Mock::given(matchers::path("/identity/connect/token")) - .respond_with(ResponseTemplate::new(200).set_body_json( - serde_json::json!({ - "access_token":"eyJhbGciOiJSUzI1NiIsImtpZCI6IjMwMURENkE1MEU4NEUxRDA5MUM4MUQzQjAwQkY5MDEwQzg1REJEOUFSUzI1NiIsInR5cCI6\ - ImF0K2p3dCIsIng1dCI6Ik1CM1dwUTZFNGRDUnlCMDdBTC1RRU1oZHZabyJ9.eyJuYmYiOjE2NzUxMDM3ODEsImV4cCI6MTY3NTEwNzM4MSwiaXNzIjo\ - iaHR0cDovL2xvY2FsaG9zdCIsImNsaWVudF9pZCI6ImVjMmMxZDQ2LTZhNGItNDc1MS1hMzEwLWFmOTYwMTMxN2YyZCIsInN1YiI6ImQzNDgwNGNhLTR\ - mNmMtNDM5Mi04NmI3LWFmOTYwMTMxNzVkMCIsIm9yZ2FuaXphdGlvbiI6ImY0ZTQ0YTdmLTExOTAtNDMyYS05ZDRhLWFmOTYwMTMxMjdjYiIsImp0aSI\ - 6IjU3QUU0NzQ0MzIwNzk1RThGQkQ4MUIxNDA2RDQyNTQyIiwiaWF0IjoxNjc1MTAzNzgxLCJzY29wZSI6WyJhcGkuc2VjcmV0cyJdfQ.GRKYzqgJZHEE\ - ZHsJkhVZH8zjYhY3hUvM4rhdV3FU10WlCteZdKHrPIadCUh-Oz9DxIAA2HfALLhj1chL4JgwPmZgPcVS2G8gk8XeBmZXowpVWJ11TXS1gYrM9syXbv9j\ - 0JUCdpeshH7e56WnlpVynyUwIum9hmYGZ_XJUfmGtlKLuNjYnawTwLEeR005uEjxq3qI1kti-WFnw8ciL4a6HLNulgiFw1dAvs4c7J0souShMfrnFO3g\ - SOHff5kKD3hBB9ynDBnJQSFYJ7dFWHIjhqs0Vj-9h0yXXCcHvu7dVGpaiNjNPxbh6YeXnY6UWcmHLDtFYsG2BWcNvVD4-VgGxXt3cMhrn7l3fSYuo32Z\ - Yk4Wop73XuxqF2fmfmBdZqGI1BafhENCcZw_bpPSfK2uHipfztrgYnrzwvzedz0rjFKbhDyrjzuRauX5dqVJ4ntPeT9g_I5n71gLxiP7eClyAx5RxdF6\ - He87NwC8i-hLBhugIvLTiDj-Sk9HvMth6zaD0ebxd56wDjq8-CMG_WcgusDqNzKFHqWNDHBXt8MLeTgZAR2rQMIMFZqFgsJlRflbig8YewmNUA9wAU74\ - TfxLY1foO7Xpg49vceB7C-PlvGi1VtX6F2i0tc_67lA5kWXnnKBPBUyspoIrmAUCwfms5nTTqA9xXAojMhRHAos_OdM", - "expires_in":3600, - "token_type":"Bearer", - "scope":"api.secrets", - "encrypted_payload":"2.E9fE8+M/VWMfhhim1KlCbQ==|eLsHR484S/tJbIkM6spnG/HP65tj9A6Tba7kAAvUp+rYuQmGLixiOCfMsqt5OvBctDfvvr/Aes\ - Bu7cZimPLyOEhqEAjn52jF0eaI38XZfeOG2VJl0LOf60Wkfh3ryAMvfvLj3G4ZCNYU8sNgoC2+IQ==|lNApuCQ4Pyakfo/wwuuajWNaEX/2MW8/3rjXB/V7n+k="}) - )), - Mock::given(matchers::path("/api/organizations/f4e44a7f-1190-432a-9d4a-af96013127cb/secrets")) - .respond_with(ResponseTemplate::new(200).set_body_json( - serde_json::json!({ - "secrets":[{ - "id":"15744a66-341a-4c62-af50-af960166b6bc", - "organizationId":"f4e44a7f-1190-432a-9d4a-af96013127cb", - "key":"2.pMS6/icTQABtulw52pq2lg==|XXbxKxDTh+mWiN1HjH2N1w==|Q6PkuT+KX/axrgN9ubD5Ajk2YNwxQkgs3WJM0S0wtG8=", - "creationDate":"2023-01-26T21:46:02.2182556Z", - "revisionDate":"2023-01-26T21:46:02.2182557Z" - }], - "projects":[], - "object":"SecretsWithProjectsList" - }) - )), - Mock::given(matchers::path("/api/secrets/15744a66-341a-4c62-af50-af960166b6bc")) - .respond_with(ResponseTemplate::new(200).set_body_json( - serde_json::json!({ - "id":"15744a66-341a-4c62-af50-af960166b6bc", - "organizationId":"f4e44a7f-1190-432a-9d4a-af96013127cb", - "key":"2.pMS6/icTQABtulw52pq2lg==|XXbxKxDTh+mWiN1HjH2N1w==|Q6PkuT+KX/axrgN9ubD5Ajk2YNwxQkgs3WJM0S0wtG8=", - "value":"2.Gl34n9JYABC7V21qHcBzHg==|c1Ds244pob7i+8+MXe4++w==|Shimz/qKMYZmzSFWdeBzFb9dFz7oF6Uv9oqkws7rEe0=", - "note":"2.Cn9ABJy7+WfR4uUHwdYepg==|+nbJyU/6hSknoa5dcEJEUg==|1DTp/ZbwGO3L3RN+VMsCHz8XDr8egn/M5iSitGGysPA=", - "creationDate":"2023-01-26T21:46:02.2182556Z", - "revisionDate":"2023-01-26T21:46:02.2182557Z", - "object":"secret" - }) - )) - ]).await; - - // Test the login is correct and we store the returned organization ID correctly - let res = client - .access_token_login(&AccessTokenLoginRequest { - access_token: "0.ec2c1d46-6a4b-4751-a310-af9601317f2d.C2IgxjjLF7qSshsbwe8JGcbM075YXw:X8vbvA0bduihIDe/qrzIQQ==".into(), - }) - .await - .unwrap(); - assert!(res.authenticated); - let organization_id = client.get_access_token_organization().unwrap(); - assert_eq!( - organization_id.to_string(), - "f4e44a7f-1190-432a-9d4a-af96013127cb" - ); - - // Test that we can retrieve the list of secrets correctly - let mut res = client - .secrets() - .list(&SecretIdentifiersRequest { organization_id }) - .await - .unwrap(); - assert_eq!(res.data.len(), 1); - - // Test that given a secret ID we can get it's data - let res = client - .secrets() - .get(&SecretGetRequest { - id: res.data.remove(0).id, - }) - .await - .unwrap(); - assert_eq!(res.key, "TEST"); - assert_eq!(res.note, "TEST"); - assert_eq!(res.value, "TEST"); - } } diff --git a/crates/bitwarden/src/client/encryption_settings.rs b/crates/bitwarden/src/client/encryption_settings.rs index e6cf3e145..7c16725e7 100644 --- a/crates/bitwarden/src/client/encryption_settings.rs +++ b/crates/bitwarden/src/client/encryption_settings.rs @@ -4,14 +4,15 @@ use rsa::RsaPrivateKey; use uuid::Uuid; #[cfg(feature = "internal")] use { - crate::{client::UserLoginMethod, crypto::KeyDecryptable}, + crate::{ + client::UserLoginMethod, + crypto::{EncString, KeyDecryptable}, + error::{CryptoError, Result}, + }, rsa::{pkcs8::DecodePrivateKey, Oaep}, }; -use crate::{ - crypto::{encrypt_aes256_hmac, EncString, SymmetricCryptoKey}, - error::{CryptoError, Result}, -}; +use crate::crypto::SymmetricCryptoKey; pub struct EncryptionSettings { user_key: SymmetricCryptoKey, @@ -26,6 +27,7 @@ impl std::fmt::Debug for EncryptionSettings { } impl EncryptionSettings { + /// Initialize the encryption settings with the user password and their encrypted keys #[cfg(feature = "internal")] pub(crate) fn new( login_method: &UserLoginMethod, @@ -44,24 +46,33 @@ impl EncryptionSettings { // Decrypt the user key let user_key = master_key.decrypt_user_key(user_key)?; - // Decrypt the private key with the user key - let private_key = { - let dec: Vec = private_key.decrypt_with_key(&user_key)?; - Some( - rsa::RsaPrivateKey::from_pkcs8_der(&dec) - .map_err(|_| CryptoError::InvalidKey)?, - ) - }; - - Ok(EncryptionSettings { - user_key, - private_key, - org_keys: HashMap::new(), - }) + Self::new_decrypted_key(user_key, private_key) } } } + /// Initialize the encryption settings with the decrypted user key and the encrypted user private key + /// This should only be used when unlocking the vault via biometrics or when the vault is set to lock: "never" + /// Otherwise handling the decrypted user key is dangerous and discouraged + #[cfg(feature = "internal")] + pub(crate) fn new_decrypted_key( + user_key: SymmetricCryptoKey, + private_key: EncString, + ) -> Result { + let private_key = { + let dec: Vec = private_key.decrypt_with_key(&user_key)?; + Some(rsa::RsaPrivateKey::from_pkcs8_der(&dec).map_err(|_| CryptoError::InvalidKey)?) + }; + + Ok(EncryptionSettings { + user_key, + private_key, + org_keys: HashMap::new(), + }) + } + + /// Initialize the encryption settings with only a single decrypted key. + /// This is used only for logging in Secrets Manager with an access token pub(crate) fn new_single_key(key: SymmetricCryptoKey) -> Self { EncryptionSettings { user_key: key, @@ -79,6 +90,10 @@ impl EncryptionSettings { let private_key = self.private_key.as_ref().ok_or(Error::VaultLocked)?; + // Make sure we only keep the keys given in the arguments and not any of the previous + // ones, which might be from organizations that the user is no longer a part of anymore + self.org_keys.clear(); + // Decrypt the org keys with the private key for (org_id, org_enc_key) in org_enc_keys { let data = match org_enc_key { @@ -109,11 +124,4 @@ impl EncryptionSettings { None => Some(&self.user_key), } } - - pub(crate) fn encrypt(&self, data: &[u8], org_id: &Option) -> Result { - let key = self.get_key(org_id).ok_or(CryptoError::NoKeyForOrg)?; - - let dec = encrypt_aes256_hmac(data, key.mac_key.ok_or(CryptoError::InvalidMac)?, key.key)?; - Ok(dec) - } } diff --git a/crates/bitwarden/src/crypto/aes_ops.rs b/crates/bitwarden/src/crypto/aes_ops.rs index 34d4021e8..182986c3b 100644 --- a/crates/bitwarden/src/crypto/aes_ops.rs +++ b/crates/bitwarden/src/crypto/aes_ops.rs @@ -1,15 +1,28 @@ +//! # AES operations +//! +//! Contains low level AES operations used by the rest of the library. +//! +//! **Warning**: Consider carefully if you have to use these functions directly, as generally we +//! expose higher level functions that are easier to use and more secure. +//! +//! In most cases you should use the [EncString] with [KeyEncryptable][super::KeyEncryptable] & +//! [KeyDecryptable][super::KeyDecryptable] instead. + use aes::cipher::{ block_padding::Pkcs7, generic_array::GenericArray, typenum::U32, BlockDecryptMut, BlockEncryptMut, KeyIvInit, }; use hmac::Mac; -use rand::RngCore; +use subtle::ConstantTimeEq; use crate::{ - crypto::{EncString, PbkdfSha256Hmac, PBKDF_SHA256_HMAC_OUT_SIZE}, + crypto::{PbkdfSha256Hmac, PBKDF_SHA256_HMAC_OUT_SIZE}, error::{CryptoError, Result}, }; +/// Decrypt using AES-256 in CBC mode. +/// +/// Behaves similar to [decrypt_aes256_hmac], but does not validate the MAC. pub fn decrypt_aes256(iv: &[u8; 16], data: Vec, key: GenericArray) -> Result> { // Decrypt data let iv = GenericArray::from_slice(iv); @@ -18,13 +31,16 @@ pub fn decrypt_aes256(iv: &[u8; 16], data: Vec, key: GenericArray) .decrypt_padded_mut::(&mut data) .map_err(|_| CryptoError::KeyDecrypt)?; - //Data is decrypted in place and returns a subslice of the original Vec, to avoid cloning it, we truncate to the subslice length + // Data is decrypted in place and returns a subslice of the original Vec, to avoid cloning it, we truncate to the subslice length let decrypted_len = decrypted_key_slice.len(); data.truncate(decrypted_len); Ok(data) } +/// Decrypt using AES-256 in CBC mode with MAC. +/// +/// Behaves similar to [decrypt_aes256], but also validates the MAC. pub fn decrypt_aes256_hmac( iv: &[u8; 16], mac: &[u8; 32], @@ -32,40 +48,67 @@ pub fn decrypt_aes256_hmac( mac_key: GenericArray, key: GenericArray, ) -> Result> { - let res = validate_mac(&mac_key, iv, &data)?; - if res != *mac { + let res = generate_mac(&mac_key, iv, &data)?; + if res.ct_ne(mac).into() { return Err(CryptoError::InvalidMac.into()); } decrypt_aes256(iv, data, key) } -pub fn encrypt_aes256(data_dec: &[u8], key: GenericArray) -> Result { - let (iv, data) = encrypt_aes256_internal(data_dec, key); +/// Encrypt using AES-256 in CBC mode. +/// +/// Behaves similar to [encrypt_aes256_hmac], but does't generate a MAC. +/// +/// ## Returns +/// +/// A AesCbc256_B64 EncString +#[allow(unused)] +pub fn encrypt_aes256(data_dec: &[u8], key: GenericArray) -> ([u8; 16], Vec) { + let rng = rand::thread_rng(); + let (iv, data) = encrypt_aes256_internal(rng, data_dec, key); - Ok(EncString::AesCbc256_B64 { iv, data }) + (iv, data) } +/// Encrypt using AES-256 in CBC mode with MAC. +/// +/// Behaves similar to [encrypt_aes256], but also generate a MAC. +/// +/// ## Returns +/// +/// A AesCbc256_HmacSha256_B64 EncString pub fn encrypt_aes256_hmac( data_dec: &[u8], mac_key: GenericArray, key: GenericArray, -) -> Result { - let (iv, data) = encrypt_aes256_internal(data_dec, key); - let mac = validate_mac(&mac_key, &iv, &data)?; +) -> Result<([u8; 16], [u8; 32], Vec)> { + let rng = rand::thread_rng(); + let (iv, data) = encrypt_aes256_internal(rng, data_dec, key); + let mac = generate_mac(&mac_key, &iv, &data)?; - Ok(EncString::AesCbc256_HmacSha256_B64 { iv, mac, data }) + Ok((iv, mac, data)) } -fn encrypt_aes256_internal(data_dec: &[u8], key: GenericArray) -> ([u8; 16], Vec) { +/// Encrypt using AES-256 in CBC mode. +/// +/// Used internally by: +/// - [encrypt_aes256] +/// - [encrypt_aes256_hmac] +fn encrypt_aes256_internal( + mut rng: impl rand::RngCore, + data_dec: &[u8], + key: GenericArray, +) -> ([u8; 16], Vec) { let mut iv = [0u8; 16]; - rand::thread_rng().fill_bytes(&mut iv); + rng.fill_bytes(&mut iv); let data = cbc::Encryptor::::new(&key, &iv.into()) .encrypt_padded_vec_mut::(data_dec); (iv, data) } -fn validate_mac(mac_key: &[u8], iv: &[u8], data: &[u8]) -> Result<[u8; 32]> { +/// Generate a MAC using HMAC-SHA256. +fn generate_mac(mac_key: &[u8], iv: &[u8], data: &[u8]) -> Result<[u8; 32]> { let mut hmac = PbkdfSha256Hmac::new_from_slice(mac_key).expect("HMAC can take key of any size"); hmac.update(iv); hmac.update(data); @@ -75,3 +118,78 @@ fn validate_mac(mac_key: &[u8], iv: &[u8], data: &[u8]) -> Result<[u8; 32]> { Ok(mac) } + +#[cfg(test)] +mod tests { + use aes::cipher::generic_array::sequence::GenericSequence; + use base64::Engine; + use rand::SeedableRng; + + use crate::util::BASE64_ENGINE; + + use super::*; + + /// Helper function for generating a `GenericArray` of size 32 with each element being + /// a multiple of a given increment, starting from a given offset. + fn generate_generic_array(offset: u8, increment: u8) -> GenericArray { + GenericArray::generate(|i| offset + i as u8 * increment) + } + + /// Helper function for generating a vector of a given size with each element being + /// a multiple of a given increment, starting from a given offset. + fn generate_vec(length: usize, offset: u8, increment: u8) -> Vec { + (0..length).map(|i| offset + i as u8 * increment).collect() + } + + #[test] + fn test_encrypt_aes256_internal() { + let key = generate_generic_array(0, 1); + + let rng = rand_chacha::ChaCha8Rng::from_seed([0u8; 32]); + let result = encrypt_aes256_internal(rng, "EncryptMe!".as_bytes(), key); + assert_eq!( + result, + ( + [62, 0, 239, 47, 137, 95, 64, 214, 127, 91, 184, 232, 31, 9, 165, 161], + vec![214, 76, 187, 97, 58, 146, 212, 140, 95, 164, 177, 204, 179, 133, 172, 148] + ) + ); + } + + #[test] + fn test_generate_mac() { + let mac_key = generate_vec(16, 0, 16); + + let iv = generate_vec(16, 0, 16); + let data = generate_vec(16, 0, 16); + + let result = generate_mac(&mac_key, &iv, &data); + + assert!(result.is_ok()); + let mac = result.unwrap(); + assert_eq!(mac.len(), 32); + } + + #[test] + fn test_decrypt_aes256() { + let iv = generate_vec(16, 0, 1); + let iv: &[u8; 16] = iv.as_slice().try_into().unwrap(); + let key = generate_generic_array(0, 1); + let data = BASE64_ENGINE.decode("ByUF8vhyX4ddU9gcooznwA==").unwrap(); + + let decrypted = decrypt_aes256(iv, data, key).unwrap(); + + assert_eq!(String::from_utf8(decrypted).unwrap(), "EncryptMe!"); + } + + #[test] + fn test_encrypt_decrypt_aes256() { + let key = generate_generic_array(0, 1); + let data = "EncryptMe!"; + + let (iv, encrypted) = encrypt_aes256(data.as_bytes(), key); + let decrypted = decrypt_aes256(&iv, encrypted, key).unwrap(); + + assert_eq!(String::from_utf8(decrypted).unwrap(), "EncryptMe!"); + } +} diff --git a/crates/bitwarden/src/crypto/enc_string.rs b/crates/bitwarden/src/crypto/enc_string.rs index 976065e60..09f877c50 100644 --- a/crates/bitwarden/src/crypto/enc_string.rs +++ b/crates/bitwarden/src/crypto/enc_string.rs @@ -1,5 +1,6 @@ use std::{fmt::Display, str::FromStr}; +use aes::cipher::{generic_array::GenericArray, typenum::U32}; use base64::Engine; use serde::{de::Visitor, Deserialize}; @@ -11,6 +12,45 @@ use crate::{ use super::{KeyDecryptable, KeyEncryptable, LocateKey}; +/// # Encrypted string primitive +/// +/// [EncString] is a Bitwarden specific primitive that represents an encrypted string. They are +/// are used together with the [KeyDecryptable] and [KeyEncryptable] traits to encrypt and decrypt +/// data using [SymmetricCryptoKey]s. +/// +/// The flexibility of the [EncString] type allows for different encryption algorithms to be used +/// which is represented by the different variants of the enum. +/// +/// ## Note +/// +/// We are currently in the progress of splitting the [EncString] into distinct AES and RSA +/// variants. To provide better control of which encryption algorithm is expected. +/// +/// For backwards compatibility we will rarely if ever be able to remove support for decrypting old +/// variants, but we should be opinionated in which variants are used for encrypting. +/// +/// ## Variants +/// - [AesCbc256_B64](EncString::AesCbc256_B64) +/// - [AesCbc128_HmacSha256_B64](EncString::AesCbc128_HmacSha256_B64) +/// - [AesCbc256_HmacSha256_B64](EncString::AesCbc256_HmacSha256_B64) +/// - [Rsa2048_OaepSha256_B64](EncString::Rsa2048_OaepSha256_B64) +/// - [Rsa2048_OaepSha1_B64](EncString::Rsa2048_OaepSha1_B64) +/// +/// ## Serialization +/// +/// [EncString] implements [Display] and [FromStr] to allow for easy serialization and uses a +/// custom scheme to represent the different variants. +/// +/// The scheme is one of the following schemes: +/// - `[type].[iv]|[data]` +/// - `[type].[iv]|[data]|[mac]` +/// - `[type].[data]` +/// +/// Where: +/// - `[type]`: is a digit number representing the variant. +/// - `[iv]`: (optional) is the initialization vector used for encryption. +/// - `[data]`: is the encrypted data. +/// - `[mac]`: (optional) is the MAC used to validate the integrity of the data. #[derive(Clone)] #[allow(unused, non_camel_case_types)] pub enum EncString { @@ -40,13 +80,14 @@ pub enum EncString { Rsa2048_OaepSha1_HmacSha256_B64 { data: Vec }, } -// We manually implement these to make sure we don't print any sensitive data +/// To avoid printing sensitive information, [EncString] debug prints to `EncString`. impl std::fmt::Debug for EncString { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { f.debug_struct("EncString").finish() } } +/// Deserializes an [EncString] from a string. impl FromStr for EncString { type Err = Error; @@ -291,6 +332,16 @@ impl serde::Serialize for EncString { } impl EncString { + pub(crate) fn encrypt_aes256_hmac( + data_dec: &[u8], + mac_key: GenericArray, + key: GenericArray, + ) -> Result { + let (iv, mac, data) = super::encrypt_aes256_hmac(data_dec, mac_key, key)?; + Ok(EncString::AesCbc256_HmacSha256_B64 { iv, mac, data }) + } + + /// The numerical representation of the encryption type of the [EncString]. const fn enc_type(&self) -> u8 { match self { EncString::AesCbc256_B64 { .. } => 0, @@ -316,7 +367,7 @@ fn invalid_len_error(expected: usize) -> impl Fn(Vec) -> EncStringParseError impl LocateKey for EncString {} impl KeyEncryptable for &[u8] { fn encrypt_with_key(self, key: &SymmetricCryptoKey) -> Result { - super::encrypt_aes256_hmac(self, key.mac_key.ok_or(CryptoError::InvalidMac)?, key.key) + EncString::encrypt_aes256_hmac(self, key.mac_key.ok_or(CryptoError::InvalidMac)?, key.key) } } diff --git a/crates/bitwarden/src/crypto/encryptable.rs b/crates/bitwarden/src/crypto/encryptable.rs index 10dbfaac6..f1bc78f15 100644 --- a/crates/bitwarden/src/crypto/encryptable.rs +++ b/crates/bitwarden/src/crypto/encryptable.rs @@ -19,10 +19,12 @@ pub trait LocateKey { } } +/// Deprecated: please use LocateKey and KeyDecryptable instead pub trait Encryptable { fn encrypt(self, enc: &EncryptionSettings, org_id: &Option) -> Result; } +/// Deprecated: please use LocateKey and KeyDecryptable instead pub trait Decryptable { fn decrypt(&self, enc: &EncryptionSettings, org_id: &Option) -> Result; } diff --git a/crates/bitwarden/src/crypto/fingerprint.rs b/crates/bitwarden/src/crypto/fingerprint.rs index 9473a2cda..ddcf89d40 100644 --- a/crates/bitwarden/src/crypto/fingerprint.rs +++ b/crates/bitwarden/src/crypto/fingerprint.rs @@ -7,6 +7,11 @@ use crate::{ wordlist::EFF_LONG_WORD_LIST, }; +/// Computes a fingerprint of the given `fingerprint_material` using the given `public_key`. +/// +/// This is commonly used for account fingerprints. With the following arguments: +/// - `fingerprint_material`: user's id. +/// - `public_key`: user's public key. pub(crate) fn fingerprint(fingerprint_material: &str, public_key: &[u8]) -> Result { let mut h = sha2::Sha256::new(); h.update(public_key); @@ -22,6 +27,7 @@ pub(crate) fn fingerprint(fingerprint_material: &str, public_key: &[u8]) -> Resu Ok(hash_word(user_fingerprint).unwrap()) } +/// Derive a 5 word phrase from a 32 byte hash. fn hash_word(hash: [u8; 32]) -> Result { let minimum_entropy = 64; diff --git a/crates/bitwarden/src/crypto/master_key.rs b/crates/bitwarden/src/crypto/master_key.rs index 064336edb..fdcf93dde 100644 --- a/crates/bitwarden/src/crypto/master_key.rs +++ b/crates/bitwarden/src/crypto/master_key.rs @@ -4,8 +4,8 @@ use rand::Rng; use sha2::Digest; use super::{ - encrypt_aes256, hkdf_expand, EncString, KeyDecryptable, PbkdfSha256Hmac, SymmetricCryptoKey, - UserKey, PBKDF_SHA256_HMAC_OUT_SIZE, + hkdf_expand, EncString, KeyDecryptable, PbkdfSha256Hmac, SymmetricCryptoKey, UserKey, + PBKDF_SHA256_HMAC_OUT_SIZE, }; use crate::{client::kdf::Kdf, error::Result, util::BASE64_ENGINE}; @@ -41,13 +41,7 @@ impl MasterKey { } pub(crate) fn make_user_key(&self) -> Result<(UserKey, EncString)> { - let mut user_key = [0u8; 64]; - rand::thread_rng().fill(&mut user_key); - - let protected = encrypt_aes256(&user_key, self.0.key)?; - - let u: &[u8] = &user_key; - Ok((UserKey::new(SymmetricCryptoKey::try_from(u)?), protected)) + make_user_key(rand::thread_rng(), self) } pub(crate) fn decrypt_user_key(&self, user_key: EncString) -> Result { @@ -58,6 +52,25 @@ impl MasterKey { } } +/// Generate a new random user key and encrypt it with the master key. +fn make_user_key( + mut rng: impl rand::RngCore, + master_key: &MasterKey, +) -> Result<(UserKey, EncString)> { + let mut user_key = [0u8; 64]; + rng.fill(&mut user_key); + + let stretched_key = stretch_master_key(master_key)?; + let protected = EncString::encrypt_aes256_hmac( + &user_key, + stretched_key.mac_key.unwrap(), + stretched_key.key, + )?; + + let u: &[u8] = &user_key; + Ok((UserKey::new(SymmetricCryptoKey::try_from(u)?), protected)) +} + /// Derive a generic key from a secret and salt using the provided KDF. fn derive_key(secret: &[u8], salt: &[u8], kdf: &Kdf) -> Result { let hash = match kdf { @@ -112,7 +125,9 @@ fn stretch_master_key(master_key: &MasterKey) -> Result { mod tests { use std::num::NonZeroU32; - use super::{stretch_master_key, HashPurpose, MasterKey}; + use rand::SeedableRng; + + use super::{make_user_key, stretch_master_key, HashPurpose, MasterKey}; use crate::{client::kdf::Kdf, crypto::SymmetricCryptoKey}; #[test] @@ -225,4 +240,47 @@ mod tests { .unwrap(), ); } + + #[test] + fn test_make_user_key() { + let mut rng = rand_chacha::ChaCha8Rng::from_seed([0u8; 32]); + + let master_key = MasterKey(SymmetricCryptoKey { + key: [ + 31, 79, 104, 226, 150, 71, 177, 90, 194, 80, 172, 209, 17, 129, 132, 81, 138, 167, + 69, 167, 254, 149, 2, 27, 39, 197, 64, 42, 22, 195, 86, 75, + ] + .into(), + mac_key: None, + }); + + let (user_key, protected) = make_user_key(&mut rng, &master_key).unwrap(); + + assert_eq!( + user_key.0.key.as_slice(), + [ + 62, 0, 239, 47, 137, 95, 64, 214, 127, 91, 184, 232, 31, 9, 165, 161, 44, 132, 14, + 195, 206, 154, 127, 59, 24, 27, 225, 136, 239, 113, 26, 30 + ] + ); + assert_eq!( + user_key.0.mac_key.unwrap().as_slice(), + [ + 152, 76, 225, 114, 185, 33, 111, 65, 159, 68, 83, 103, 69, 109, 86, 25, 49, 74, 66, + 163, 218, 134, 176, 1, 56, 123, 253, 184, 14, 12, 254, 66 + ] + ); + + // Ensure we can decrypt the key and get back the same key + let decrypted = master_key.decrypt_user_key(protected).unwrap(); + + assert_eq!( + decrypted.key, user_key.0.key, + "Decrypted key doesn't match user key" + ); + assert_eq!( + decrypted.mac_key, user_key.0.mac_key, + "Decrypted key doesn't match user key" + ); + } } diff --git a/crates/bitwarden/src/crypto/mod.rs b/crates/bitwarden/src/crypto/mod.rs index d8a1f0557..b030a5107 100644 --- a/crates/bitwarden/src/crypto/mod.rs +++ b/crates/bitwarden/src/crypto/mod.rs @@ -33,7 +33,7 @@ pub use encryptable::{Decryptable, Encryptable, LocateKey}; mod key_encryptable; pub use key_encryptable::{KeyDecryptable, KeyEncryptable}; mod aes_ops; -pub use aes_ops::{decrypt_aes256, decrypt_aes256_hmac, encrypt_aes256, encrypt_aes256_hmac}; +use aes_ops::{decrypt_aes256_hmac, encrypt_aes256_hmac}; mod symmetric_crypto_key; pub use symmetric_crypto_key::SymmetricCryptoKey; mod shareable_key; diff --git a/crates/bitwarden/src/crypto/rsa.rs b/crates/bitwarden/src/crypto/rsa.rs index 0d2d135b9..f105dbfe2 100644 --- a/crates/bitwarden/src/crypto/rsa.rs +++ b/crates/bitwarden/src/crypto/rsa.rs @@ -5,7 +5,7 @@ use rsa::{ }; use crate::{ - crypto::{encrypt_aes256_hmac, EncString, SymmetricCryptoKey}, + crypto::{EncString, SymmetricCryptoKey}, error::{Error, Result}, util::BASE64_ENGINE, }; @@ -33,7 +33,7 @@ pub(super) fn make_key_pair(key: &SymmetricCryptoKey) -> Result { .to_pkcs8_der() .map_err(|_| Error::Internal("unable to create private key"))?; - let protected = encrypt_aes256_hmac(pkcs.as_bytes(), key.mac_key.unwrap(), key.key)?; + let protected = EncString::encrypt_aes256_hmac(pkcs.as_bytes(), key.mac_key.unwrap(), key.key)?; Ok(RsaKeyPair { public: b64, diff --git a/crates/bitwarden/src/crypto/user_key.rs b/crates/bitwarden/src/crypto/user_key.rs index 0fe560665..7f5bae413 100644 --- a/crates/bitwarden/src/crypto/user_key.rs +++ b/crates/bitwarden/src/crypto/user_key.rs @@ -6,7 +6,7 @@ use crate::{ error::Result, }; -pub(crate) struct UserKey(SymmetricCryptoKey); +pub(crate) struct UserKey(pub(super) SymmetricCryptoKey); impl UserKey { pub(crate) fn new(key: SymmetricCryptoKey) -> Self { diff --git a/crates/bitwarden/src/lib.rs b/crates/bitwarden/src/lib.rs index a61857992..20e36d237 100644 --- a/crates/bitwarden/src/lib.rs +++ b/crates/bitwarden/src/lib.rs @@ -39,7 +39,7 @@ //! //! // Before we operate, we need to authenticate with a token //! let token = AccessTokenLoginRequest { access_token: String::from("") }; -//! client.access_token_login(&token).await.unwrap(); +//! client.auth().login_access_token(&token).await.unwrap(); //! //! let org_id = SecretIdentifiersRequest { organization_id: Uuid::parse_str("00000000-0000-0000-0000-000000000000").unwrap() }; //! println!("Stored secrets: {:#?}", client.secrets().list(&org_id).await.unwrap()); diff --git a/crates/bitwarden/src/mobile/client_crypto.rs b/crates/bitwarden/src/mobile/client_crypto.rs index 67f69d8f1..118e02726 100644 --- a/crates/bitwarden/src/mobile/client_crypto.rs +++ b/crates/bitwarden/src/mobile/client_crypto.rs @@ -2,7 +2,10 @@ use crate::Client; #[cfg(feature = "internal")] use crate::{ error::Result, - mobile::crypto::{initialize_crypto, InitCryptoRequest}, + mobile::crypto::{ + get_user_encryption_key, initialize_org_crypto, initialize_user_crypto, + InitOrgCryptoRequest, InitUserCryptoRequest, + }, }; pub struct ClientCrypto<'a> { @@ -11,8 +14,18 @@ pub struct ClientCrypto<'a> { impl<'a> ClientCrypto<'a> { #[cfg(feature = "internal")] - pub async fn initialize_crypto(&mut self, req: InitCryptoRequest) -> Result<()> { - initialize_crypto(self.client, req).await + pub async fn initialize_user_crypto(&mut self, req: InitUserCryptoRequest) -> Result<()> { + initialize_user_crypto(self.client, req).await + } + + #[cfg(feature = "internal")] + pub async fn initialize_org_crypto(&mut self, req: InitOrgCryptoRequest) -> Result<()> { + initialize_org_crypto(self.client, req).await + } + + #[cfg(feature = "internal")] + pub async fn get_user_encryption_key(&mut self) -> Result { + get_user_encryption_key(self.client).await } } diff --git a/crates/bitwarden/src/mobile/crypto.rs b/crates/bitwarden/src/mobile/crypto.rs index f4d26dc6c..9c2c8478f 100644 --- a/crates/bitwarden/src/mobile/crypto.rs +++ b/crates/bitwarden/src/mobile/crypto.rs @@ -3,29 +3,47 @@ use std::collections::HashMap; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; -use crate::{client::kdf::Kdf, crypto::EncString, error::Result, Client}; +use crate::{ + client::kdf::Kdf, + crypto::EncString, + error::{Error, Result}, + Client, +}; #[cfg(feature = "internal")] #[derive(Serialize, Deserialize, Debug, JsonSchema)] #[serde(rename_all = "camelCase", deny_unknown_fields)] #[cfg_attr(feature = "mobile", derive(uniffi::Record))] -pub struct InitCryptoRequest { +pub struct InitUserCryptoRequest { /// The user's KDF parameters, as received from the prelogin request pub kdf_params: Kdf, /// The user's email address pub email: String, - /// The user's master password - pub password: String, - /// The user's encrypted symmetric crypto key - pub user_key: String, - /// The user's encryptred private key + /// The user's encrypted private key pub private_key: String, - /// The encryption keys for all the organizations the user is a part of - pub organization_keys: HashMap, + /// The initialization method to use + pub method: InitUserCryptoMethod, +} + +#[cfg(feature = "internal")] +#[derive(Serialize, Deserialize, Debug, JsonSchema)] +#[serde(rename_all = "camelCase", deny_unknown_fields)] +#[cfg_attr(feature = "mobile", derive(uniffi::Enum))] +pub enum InitUserCryptoMethod { + Password { + /// The user's master password + password: String, + /// The user's encrypted symmetric crypto key + user_key: String, + }, + DecryptedKey { + /// The user's decrypted encryption key, obtained using `get_user_encryption_key` + decrypted_user_key: String, + }, } #[cfg(feature = "internal")] -pub async fn initialize_crypto(client: &mut Client, req: InitCryptoRequest) -> Result<()> { +pub async fn initialize_user_crypto(client: &mut Client, req: InitUserCryptoRequest) -> Result<()> { let login_method = crate::client::LoginMethod::User(crate::client::UserLoginMethod::Username { client_id: "".to_string(), email: req.email, @@ -33,18 +51,43 @@ pub async fn initialize_crypto(client: &mut Client, req: InitCryptoRequest) -> R }); client.set_login_method(login_method); - let user_key = req.user_key.parse::()?; - let private_key = req.private_key.parse::()?; + let private_key: EncString = req.private_key.parse()?; - client.initialize_user_crypto(&req.password, user_key, private_key)?; + match req.method { + InitUserCryptoMethod::Password { password, user_key } => { + let user_key: EncString = user_key.parse()?; + client.initialize_user_crypto(&password, user_key, private_key)?; + } + InitUserCryptoMethod::DecryptedKey { decrypted_user_key } => { + client.initialize_user_crypto_decrypted_key(&decrypted_user_key, private_key)?; + } + } - let organization_keys = req - .organization_keys - .into_iter() - .map(|(k, v)| Ok((k, v.parse::()?))) - .collect::>>()?; + Ok(()) +} - client.initialize_org_crypto(organization_keys)?; +#[cfg(feature = "internal")] +#[derive(Serialize, Deserialize, Debug, JsonSchema)] +#[serde(rename_all = "camelCase", deny_unknown_fields)] +#[cfg_attr(feature = "mobile", derive(uniffi::Record))] +pub struct InitOrgCryptoRequest { + /// The encryption keys for all the organizations the user is a part of + pub organization_keys: HashMap, +} +#[cfg(feature = "internal")] +pub async fn initialize_org_crypto(client: &mut Client, req: InitOrgCryptoRequest) -> Result<()> { + let organization_keys = req.organization_keys.into_iter().collect(); + client.initialize_org_crypto(organization_keys)?; Ok(()) } + +#[cfg(feature = "internal")] +pub async fn get_user_encryption_key(client: &mut Client) -> Result { + let user_key = client + .get_encryption_settings()? + .get_key(&None) + .ok_or(Error::VaultLocked)?; + + Ok(user_key.to_base64()) +} diff --git a/crates/bitwarden/src/mobile/mod.rs b/crates/bitwarden/src/mobile/mod.rs index ad2d794af..fe3083aa8 100644 --- a/crates/bitwarden/src/mobile/mod.rs +++ b/crates/bitwarden/src/mobile/mod.rs @@ -3,8 +3,11 @@ pub mod crypto; pub mod kdf; pub mod vault; -pub(crate) mod client_crypto; -pub(crate) mod client_kdf; +mod client_crypto; +mod client_kdf; + +pub use client_crypto::ClientCrypto; +pub use client_kdf::ClientKdf; // Usually we wouldn't want to expose EncStrings in the API or the schemas, // but we need them in the mobile API, so define it here to limit the scope diff --git a/crates/bitwarden/src/mobile/vault/mod.rs b/crates/bitwarden/src/mobile/vault/mod.rs index f22f004ce..97f9556af 100644 --- a/crates/bitwarden/src/mobile/vault/mod.rs +++ b/crates/bitwarden/src/mobile/vault/mod.rs @@ -5,3 +5,10 @@ mod client_password_history; mod client_sends; mod client_totp; mod client_vault; + +pub use client_ciphers::ClientCiphers; +pub use client_collection::ClientCollections; +pub use client_folders::ClientFolders; +pub use client_password_history::ClientPasswordHistory; +pub use client_sends::ClientSends; +pub use client_vault::ClientVault; diff --git a/crates/bitwarden/src/platform/generate_fingerprint.rs b/crates/bitwarden/src/platform/generate_fingerprint.rs index 800d6e847..e9562ab34 100644 --- a/crates/bitwarden/src/platform/generate_fingerprint.rs +++ b/crates/bitwarden/src/platform/generate_fingerprint.rs @@ -7,6 +7,7 @@ use crate::{crypto::fingerprint, error::Result, util::BASE64_ENGINE}; #[derive(Serialize, Deserialize, Debug, JsonSchema)] #[serde(rename_all = "camelCase", deny_unknown_fields)] +#[cfg_attr(feature = "mobile", derive(uniffi::Record))] pub struct FingerprintRequest { /// The input material, used in the fingerprint generation process. pub fingerprint_material: String, diff --git a/crates/bitwarden/src/secrets_manager/mod.rs b/crates/bitwarden/src/secrets_manager/mod.rs index 0afbfe38c..27b84121e 100644 --- a/crates/bitwarden/src/secrets_manager/mod.rs +++ b/crates/bitwarden/src/secrets_manager/mod.rs @@ -3,3 +3,6 @@ pub mod secrets; mod client_projects; mod client_secrets; + +pub use client_projects::ClientProjects; +pub use client_secrets::ClientSecrets; diff --git a/crates/bitwarden/src/secrets_manager/projects/create.rs b/crates/bitwarden/src/secrets_manager/projects/create.rs index 3ba6ecd36..996a3463e 100644 --- a/crates/bitwarden/src/secrets_manager/projects/create.rs +++ b/crates/bitwarden/src/secrets_manager/projects/create.rs @@ -4,7 +4,11 @@ use serde::{Deserialize, Serialize}; use uuid::Uuid; use super::ProjectResponse; -use crate::{client::Client, error::Result}; +use crate::{ + client::Client, + crypto::KeyEncryptable, + error::{Error, Result}, +}; #[derive(Serialize, Deserialize, Debug, JsonSchema)] #[serde(rename_all = "camelCase", deny_unknown_fields)] @@ -19,12 +23,13 @@ pub(crate) async fn create_project( client: &mut Client, input: &ProjectCreateRequest, ) -> Result { - let enc = client.get_encryption_settings()?; - - let org_id = Some(input.organization_id); + let key = client + .get_encryption_settings()? + .get_key(&Some(input.organization_id)) + .ok_or(Error::VaultLocked)?; let project = Some(ProjectCreateRequestModel { - name: enc.encrypt(input.name.as_bytes(), &org_id)?.to_string(), + name: input.name.clone().encrypt_with_key(key)?.to_string(), }); let config = client.get_api_configurations().await; diff --git a/crates/bitwarden/src/secrets_manager/projects/update.rs b/crates/bitwarden/src/secrets_manager/projects/update.rs index b0a040c96..6a0479d88 100644 --- a/crates/bitwarden/src/secrets_manager/projects/update.rs +++ b/crates/bitwarden/src/secrets_manager/projects/update.rs @@ -4,7 +4,11 @@ use serde::{Deserialize, Serialize}; use uuid::Uuid; use super::ProjectResponse; -use crate::{client::Client, error::Result}; +use crate::{ + client::Client, + crypto::KeyEncryptable, + error::{Error, Result}, +}; #[derive(Serialize, Deserialize, Debug, JsonSchema)] #[serde(rename_all = "camelCase", deny_unknown_fields)] @@ -21,12 +25,13 @@ pub(crate) async fn update_project( client: &mut Client, input: &ProjectPutRequest, ) -> Result { - let enc = client.get_encryption_settings()?; - - let org_id = Some(input.organization_id); + let key = client + .get_encryption_settings()? + .get_key(&Some(input.organization_id)) + .ok_or(Error::VaultLocked)?; let project = Some(ProjectUpdateRequestModel { - name: enc.encrypt(input.name.as_bytes(), &org_id)?.to_string(), + name: input.name.clone().encrypt_with_key(key)?.to_string(), }); let config = client.get_api_configurations().await; diff --git a/crates/bitwarden/src/secrets_manager/secrets/create.rs b/crates/bitwarden/src/secrets_manager/secrets/create.rs index ddec3abd4..a1bb81799 100644 --- a/crates/bitwarden/src/secrets_manager/secrets/create.rs +++ b/crates/bitwarden/src/secrets_manager/secrets/create.rs @@ -4,7 +4,11 @@ use serde::{Deserialize, Serialize}; use uuid::Uuid; use super::SecretResponse; -use crate::{error::Result, Client}; +use crate::{ + crypto::KeyEncryptable, + error::{Error, Result}, + Client, +}; #[derive(Serialize, Deserialize, Debug, JsonSchema)] #[serde(rename_all = "camelCase", deny_unknown_fields)] @@ -24,14 +28,15 @@ pub(crate) async fn create_secret( client: &mut Client, input: &SecretCreateRequest, ) -> Result { - let enc = client.get_encryption_settings()?; - - let org_id = Some(input.organization_id); + let key = client + .get_encryption_settings()? + .get_key(&Some(input.organization_id)) + .ok_or(Error::VaultLocked)?; let secret = Some(SecretCreateRequestModel { - key: enc.encrypt(input.key.as_bytes(), &org_id)?.to_string(), - value: enc.encrypt(input.value.as_bytes(), &org_id)?.to_string(), - note: enc.encrypt(input.note.as_bytes(), &org_id)?.to_string(), + key: input.key.clone().encrypt_with_key(key)?.to_string(), + value: input.value.clone().encrypt_with_key(key)?.to_string(), + note: input.note.clone().encrypt_with_key(key)?.to_string(), project_ids: input.project_ids.clone(), }); diff --git a/crates/bitwarden/src/secrets_manager/secrets/update.rs b/crates/bitwarden/src/secrets_manager/secrets/update.rs index b1a82bad8..970812c5a 100644 --- a/crates/bitwarden/src/secrets_manager/secrets/update.rs +++ b/crates/bitwarden/src/secrets_manager/secrets/update.rs @@ -4,7 +4,11 @@ use serde::{Deserialize, Serialize}; use uuid::Uuid; use super::SecretResponse; -use crate::{client::Client, error::Result}; +use crate::{ + client::Client, + crypto::KeyEncryptable, + error::{Error, Result}, +}; #[derive(Serialize, Deserialize, Debug, JsonSchema)] #[serde(rename_all = "camelCase", deny_unknown_fields)] @@ -24,14 +28,15 @@ pub(crate) async fn update_secret( client: &mut Client, input: &SecretPutRequest, ) -> Result { - let enc = client.get_encryption_settings()?; - - let org_id = Some(input.organization_id); + let key = client + .get_encryption_settings()? + .get_key(&Some(input.organization_id)) + .ok_or(Error::VaultLocked)?; let secret = Some(SecretUpdateRequestModel { - key: enc.encrypt(input.key.as_bytes(), &org_id)?.to_string(), - value: enc.encrypt(input.value.as_bytes(), &org_id)?.to_string(), - note: enc.encrypt(input.note.as_bytes(), &org_id)?.to_string(), + key: input.key.clone().encrypt_with_key(key)?.to_string(), + value: input.value.clone().encrypt_with_key(key)?.to_string(), + note: input.note.clone().encrypt_with_key(key)?.to_string(), project_ids: input.project_ids.clone(), }); diff --git a/crates/bitwarden/src/tool/exporters/mod.rs b/crates/bitwarden/src/tool/exporters/mod.rs index 508aae8fb..1170dbd38 100644 --- a/crates/bitwarden/src/tool/exporters/mod.rs +++ b/crates/bitwarden/src/tool/exporters/mod.rs @@ -6,6 +6,7 @@ use crate::{ }; mod client_exporter; +pub use client_exporter::ClientExporters; #[derive(JsonSchema)] #[cfg_attr(feature = "mobile", derive(uniffi::Enum))] diff --git a/crates/bitwarden/src/tool/generators/mod.rs b/crates/bitwarden/src/tool/generators/mod.rs index bdc0fb260..4d991b765 100644 --- a/crates/bitwarden/src/tool/generators/mod.rs +++ b/crates/bitwarden/src/tool/generators/mod.rs @@ -1,4 +1,5 @@ mod client_generator; mod password; +pub use client_generator::ClientGenerator; pub use password::{PassphraseGeneratorRequest, PasswordGeneratorRequest}; diff --git a/crates/bitwarden/src/tool/mod.rs b/crates/bitwarden/src/tool/mod.rs index 2130a6b0c..212ce4bf7 100644 --- a/crates/bitwarden/src/tool/mod.rs +++ b/crates/bitwarden/src/tool/mod.rs @@ -1,5 +1,5 @@ mod exporters; mod generators; -pub use exporters::ExportFormat; -pub use generators::{PassphraseGeneratorRequest, PasswordGeneratorRequest}; +pub use exporters::{ClientExporters, ExportFormat}; +pub use generators::{ClientGenerator, PassphraseGeneratorRequest, PasswordGeneratorRequest}; diff --git a/crates/bitwarden/src/util.rs b/crates/bitwarden/src/util.rs index fc9f30447..b6c8465ec 100644 --- a/crates/bitwarden/src/util.rs +++ b/crates/bitwarden/src/util.rs @@ -3,11 +3,8 @@ use std::num::NonZeroU32; use base64::{ alphabet, engine::{DecodePaddingMode, GeneralPurpose, GeneralPurposeConfig}, - Engine, }; -use crate::error::Result; - pub fn default_pbkdf2_iterations() -> NonZeroU32 { NonZeroU32::new(600_000).unwrap() } @@ -24,14 +21,6 @@ pub fn default_argon2_parallelism() -> NonZeroU32 { NonZeroU32::new(4).unwrap() } -#[derive(serde::Deserialize)] -pub struct JWTToken { - pub sub: String, - pub email: Option, - pub organization: Option, - pub scope: Vec, -} - const BASE64_ENGINE_CONFIG: GeneralPurposeConfig = GeneralPurposeConfig::new() .with_encode_padding(true) .with_decode_padding_mode(DecodePaddingMode::Indifferent); @@ -39,48 +28,6 @@ const BASE64_ENGINE_CONFIG: GeneralPurposeConfig = GeneralPurposeConfig::new() pub const BASE64_ENGINE: GeneralPurpose = GeneralPurpose::new(&alphabet::STANDARD, BASE64_ENGINE_CONFIG); -pub fn decode_token(token: &str) -> Result { - let split = token.split('.').collect::>(); - if split.len() != 3 { - return Err(crate::error::Error::Internal( - "JWT token has an invalid number of parts", - )); - } - let decoded = BASE64_ENGINE.decode(split[1])?; - Ok(serde_json::from_slice(&decoded)?) -} - -#[cfg(test)] -mod tests { - #[test] - fn can_decode_jwt() { - let jwt = "eyJhbGciOiJSUzI1NiIsImtpZCI6IjMwMURENkE1MEU4NEUxRDA5MUM4MUQzQjAwQkY5MDEwQz\ - g1REJEOUFSUzI1NiIsInR5cCI6ImF0K2p3dCIsIng1dCI6Ik1CM1dwUTZFNGRDUnlCMDdBTC1RRU1oZHZabyJ9.eyJu\ - YmYiOjE2NzUxMDM1NzcsImV4cCI6MTY3NTEwNzE3NywiaXNzIjoiaHR0cDovL2xvY2FsaG9zdCIsImNsaWVudF9pZCI\ - 6IndlYiIsInN1YiI6ImUyNWQzN2YzLWI2MDMtNDBkZS04NGJhLWFmOTYwMTJmNWE0MiIsImF1dGhfdGltZSI6MTY3NT\ - EwMzU0OSwiaWRwIjoiYml0d2FyZGVuIiwicHJlbWl1bSI6ZmFsc2UsImVtYWlsIjoidGVzdEBiaXR3YXJkZW4uY29tI\ - iwiZW1haWxfdmVyaWZpZWQiOnRydWUsInNzdGFtcCI6IkUzNElDWVhRUFRDS01EVldBREZDNktHNDJCQldJRDdJIiwi\ - bmFtZSI6IlRlc3QiLCJvcmdvd25lciI6ImY0ZTQ0YTdmLTExOTAtNDMyYS05ZDRhLWFmOTYwMTMxMjdjYiIsImRldml\ - jZSI6Ijg5Mjg5M2FiLWRkNDMtNDUwYS04NGI1LWFhOWM1YjdiYjJkOCIsImp0aSI6IkEzMkVFNjY5NDdEQzlDNUE2MT\ - IwRURBRTIwNzc5OUJFIiwiaWF0IjoxNjc1MTAzNTc3LCJzY29wZSI6WyJhcGkiLCJvZmZsaW5lX2FjY2VzcyJdLCJhb\ - XIiOlsiQXBwbGljYXRpb24iXX0.AyDkKvjmyaSPQViQSa2sGTKIkDGrUAtDmwpE57K4DDWT0QvwDe7FMktmwiF4LH36\ - wx_FnpH21VI1pzwJeTHXtaz3niANJtQZjzGFsNAna_95vrsxZC2YizgGlt6mX4YIGmAw9DiYrmaN0BvQOEm_caV_u6f\ - a30iz9Kvjxf7cpzeZvPEysxGpB3k3TRYTkFUdV43HiXdhXMBhyyOpFU6Fk6yA41y7-8bGYc5mYGknWktmPD9Yx-1xKL\ - ftFja1SnCoLPWvDeK60lqWZQiT4tZHCYJ7m0bBNCccYHc2Kk2Bo5-UoyDxazPwsqMxeNfjlaUuj3o5N_uQ-4n_gVbeA\ - qWV2wrel5UhYjWnczMSLBtt9p0W35kkBPt3ZAnRWMtQMPNH04p-_L6cG-Xu6lDksBTwaavcmtnCKG8V91826EiQ8MrF\ - wGWQRZV6tPKTDAYCgSAZGBY3QDmPGT5BeFcg5Ag_nYYIIifKP-kv10v_N-TOcT3NeGBOUlAZ-9m7iT7Rk3vC--SDZdA\ - U5turoBFiiPL2XXfAjM7P0r7J91gfXc0FaD6I2jDxOmym5h7Yn5phLsbC2NlIXkZp54dKHICenPl4ve6ndDIJacVeS5\ - f3LEddAPV8cAFza4DjA8pZJLFrMyRvMXcL_PjKF8qPVzqVWh03lfJ4clOIxR2gOuWIc902Y5E"; - - let token = super::decode_token(jwt).unwrap(); - assert_eq!(token.sub, "e25d37f3-b603-40de-84ba-af96012f5a42"); - assert_eq!(token.email.as_deref(), Some("test@bitwarden.com")); - assert_eq!(token.organization.as_deref(), None); - assert_eq!(token.scope[0], "api"); - assert_eq!(token.scope[1], "offline_access"); - } -} - #[cfg(test)] pub async fn start_mock(mocks: Vec) -> (wiremock::MockServer, crate::Client) { let server = wiremock::MockServer::start().await; diff --git a/crates/bitwarden/src/vault/cipher/cipher.rs b/crates/bitwarden/src/vault/cipher/cipher.rs index b01f17136..b95a2f090 100644 --- a/crates/bitwarden/src/vault/cipher/cipher.rs +++ b/crates/bitwarden/src/vault/cipher/cipher.rs @@ -43,6 +43,9 @@ pub struct Cipher { pub folder_id: Option, pub collection_ids: Vec, + /// More recent ciphers uses individual encryption keys to encrypt the other fields of the Cipher. + pub key: Option, + pub name: EncString, pub notes: Option, @@ -77,6 +80,8 @@ pub struct CipherView { pub folder_id: Option, pub collection_ids: Vec, + pub key: Option, + pub name: String, pub notes: Option, @@ -131,11 +136,15 @@ pub struct CipherListView { impl KeyEncryptable for CipherView { fn encrypt_with_key(self, key: &SymmetricCryptoKey) -> Result { + let ciphers_key = Cipher::get_cipher_key(key, &self.key)?; + let key = ciphers_key.as_ref().unwrap_or(key); + Ok(Cipher { id: self.id, organization_id: self.organization_id, folder_id: self.folder_id, collection_ids: self.collection_ids, + key: self.key, name: self.name.encrypt_with_key(key)?, notes: self.notes.encrypt_with_key(key)?, r#type: self.r#type, @@ -161,11 +170,15 @@ impl KeyEncryptable for CipherView { impl KeyDecryptable for Cipher { fn decrypt_with_key(&self, key: &SymmetricCryptoKey) -> Result { + let ciphers_key = Cipher::get_cipher_key(key, &self.key)?; + let key = ciphers_key.as_ref().unwrap_or(key); + Ok(CipherView { id: self.id, organization_id: self.organization_id, folder_id: self.folder_id, collection_ids: self.collection_ids.clone(), + key: self.key.clone(), name: self.name.decrypt_with_key(key)?, notes: self.notes.decrypt_with_key(key)?, r#type: self.r#type, @@ -190,6 +203,23 @@ impl KeyDecryptable for Cipher { } impl Cipher { + /// Get the decrypted individual encryption key for this cipher. + /// Note that some ciphers do not have individual encryption keys, + /// in which case this will return Ok(None) and the key associated + /// with this cipher's user or organization must be used instead + fn get_cipher_key( + key: &SymmetricCryptoKey, + ciphers_key: &Option, + ) -> Result> { + ciphers_key + .as_ref() + .map(|k| { + let key: Vec = k.decrypt_with_key(key)?; + SymmetricCryptoKey::try_from(key.as_slice()) + }) + .transpose() + } + fn get_decrypted_subtitle(&self, key: &SymmetricCryptoKey) -> Result { Ok(match self.r#type { CipherType::Login => { @@ -257,6 +287,9 @@ impl Cipher { impl KeyDecryptable for Cipher { fn decrypt_with_key(&self, key: &SymmetricCryptoKey) -> Result { + let ciphers_key = Cipher::get_cipher_key(key, &self.key)?; + let key = ciphers_key.as_ref().unwrap_or(key); + Ok(CipherListView { id: self.id, organization_id: self.organization_id, diff --git a/crates/bitwarden/tests/register.rs b/crates/bitwarden/tests/register.rs new file mode 100644 index 000000000..956fe86ce --- /dev/null +++ b/crates/bitwarden/tests/register.rs @@ -0,0 +1,41 @@ +/// Integration test for registering a new user and unlocking the vault +#[cfg(feature = "mobile")] +#[tokio::test] +async fn test_register_initialize_crypto() { + use std::num::NonZeroU32; + + use bitwarden::{ + client::kdf::Kdf, + mobile::crypto::{InitUserCryptoMethod, InitUserCryptoRequest}, + Client, + }; + + let mut client = Client::new(None); + + let email = "test@bitwarden.com"; + let password = "test123"; + let kdf = Kdf::PBKDF2 { + iterations: NonZeroU32::new(600_000).unwrap(), + }; + + let register_response = client + .auth() + .make_register_keys(email.to_owned(), password.to_owned(), kdf.clone()) + .unwrap(); + + // Ensure we can initialize the crypto with the new keys + client + .crypto() + .initialize_user_crypto(InitUserCryptoRequest { + kdf_params: kdf, + email: email.to_owned(), + private_key: register_response.keys.private.to_string(), + + method: InitUserCryptoMethod::Password { + password: password.to_owned(), + user_key: register_response.encrypted_user_key, + }, + }) + .await + .unwrap(); +} diff --git a/crates/bitwarden/uniffi.toml b/crates/bitwarden/uniffi.toml index 5c66e5382..bc4a9d9ec 100644 --- a/crates/bitwarden/uniffi.toml +++ b/crates/bitwarden/uniffi.toml @@ -1,6 +1,8 @@ [bindings.kotlin] package_name = "com.bitwarden.core" +generate_immutable_records = true [bindings.swift] ffi_module_name = "BitwardenCoreFFI" module_name = "BitwardenCore" +generate_immutable_records = true diff --git a/crates/bw/Cargo.toml b/crates/bw/Cargo.toml index 0793470b3..9ceb2b153 100644 --- a/crates/bw/Cargo.toml +++ b/crates/bw/Cargo.toml @@ -12,15 +12,13 @@ Bitwarden Password Manager CLI """ keywords = ["bitwarden", "password-manager", "cli"] -# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html - [dependencies] -clap = { version = "4.3.0", features = ["derive", "env"] } -tokio = { version = "1.28.2", features = ["rt-multi-thread", "macros"] } -log = "0.4.18" -env_logger = "0.10.0" +clap = { version = "4.4.8", features = ["derive", "env"] } color-eyre = "0.6" +env_logger = "0.10.1" inquire = "0.6.2" +log = "0.4.20" +tokio = { version = "1.34.0", features = ["rt-multi-thread", "macros"] } bitwarden = { path = "../bitwarden", version = "0.3.1", features = [ "internal", @@ -29,4 +27,4 @@ bitwarden = { path = "../bitwarden", version = "0.3.1", features = [ bitwarden-cli = { path = "../bitwarden-cli", version = "0.1.0" } [dev-dependencies] -tempfile = "3.5.0" +tempfile = "3.8.1" diff --git a/crates/bw/src/auth/login.rs b/crates/bw/src/auth/login.rs index 1c169817f..1fcd15414 100644 --- a/crates/bw/src/auth/login.rs +++ b/crates/bw/src/auth/login.rs @@ -10,15 +10,16 @@ use color_eyre::eyre::{bail, Result}; use inquire::{Password, Text}; use log::{debug, error, info}; -pub(crate) async fn password_login(mut client: Client, email: Option) -> Result<()> { +pub(crate) async fn login_password(mut client: Client, email: Option) -> Result<()> { let email = text_prompt_when_none("Email", email)?; let password = Password::new("Password").without_confirmation().prompt()?; - let kdf = client.prelogin(email.clone()).await?; + let kdf = client.auth().prelogin(email.clone()).await?; let result = client - .password_login(&PasswordLoginRequest { + .auth() + .login_password(&PasswordLoginRequest { email: email.clone(), password: password.clone(), two_factor: None, @@ -45,6 +46,7 @@ pub(crate) async fn password_login(mut client: Client, email: Option) -> } else if let Some(tf) = two_factor.email { // Send token client + .auth() .send_two_factor_email(&TwoFactorEmailRequest { email: email.clone(), password: password.clone(), @@ -64,7 +66,8 @@ pub(crate) async fn password_login(mut client: Client, email: Option) -> }; let result = client - .password_login(&PasswordLoginRequest { + .auth() + .login_password(&PasswordLoginRequest { email, password, two_factor, @@ -80,7 +83,7 @@ pub(crate) async fn password_login(mut client: Client, email: Option) -> Ok(()) } -pub(crate) async fn api_key_login( +pub(crate) async fn login_api_key( mut client: Client, client_id: Option, client_secret: Option, @@ -91,7 +94,8 @@ pub(crate) async fn api_key_login( let password = Password::new("Password").without_confirmation().prompt()?; let result = client - .api_key_login(&ApiKeyLoginRequest { + .auth() + .login_api_key(&ApiKeyLoginRequest { client_id, client_secret, password, diff --git a/crates/bw/src/auth/mod.rs b/crates/bw/src/auth/mod.rs index a745a70f0..a4c7e2ed5 100644 --- a/crates/bw/src/auth/mod.rs +++ b/crates/bw/src/auth/mod.rs @@ -1,2 +1,2 @@ mod login; -pub(crate) use login::{api_key_login, password_login}; +pub(crate) use login::{login_api_key, login_password}; diff --git a/crates/bw/src/main.rs b/crates/bw/src/main.rs index 73e64a4aa..609650ab1 100644 --- a/crates/bw/src/main.rs +++ b/crates/bw/src/main.rs @@ -143,12 +143,12 @@ async fn process_commands() -> Result<()> { match args.command { // FIXME: Rust CLI will not support password login! LoginCommands::Password { email } => { - auth::password_login(client, email).await?; + auth::login_password(client, email).await?; } LoginCommands::ApiKey { client_id, client_secret, - } => auth::api_key_login(client, client_id, client_secret).await?, + } => auth::login_api_key(client, client_id, client_secret).await?, } return Ok(()); } diff --git a/crates/bws/Cargo.toml b/crates/bws/Cargo.toml index 00ec40da7..95dbef003 100644 --- a/crates/bws/Cargo.toml +++ b/crates/bws/Cargo.toml @@ -12,36 +12,38 @@ Bitwarden Secrets Manager CLI """ keywords = ["bitwarden", "secrets-manager", "cli"] -# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html - [dependencies] bat = { version = "0.24.0", features = [ "regex-onig", ], default-features = false } -chrono = { version = "0.4.26", features = [ +chrono = { version = "0.4.31", features = [ "clock", "std", ], default-features = false } -clap = { version = "4.3.0", features = ["derive", "env", "string"] } -clap_complete = "4.3.2" +clap = { version = "4.4.8", features = ["derive", "env", "string"] } +clap_complete = "4.4.4" color-eyre = "0.6" -comfy-table = "^7.0.1" +comfy-table = "^7.1.0" directories = "5.0.1" -env_logger = "0.10.0" -log = "0.4.18" -serde = "^1.0.163" -serde_json = "^1.0.96" +env_logger = "0.10.1" +log = "0.4.20" +regex = { version = "1.10.2", features = [ + "std", + "perf", +], default-features = false } +serde = "^1.0.193" +serde_json = "^1.0.108" serde_yaml = "0.9" -supports-color = "2.0.0" -thiserror = "1.0.40" -tokio = { version = "1.28.2", features = ["rt-multi-thread", "macros"] } -toml = "0.8.0" -uuid = { version = "^1.3.3", features = ["serde"] } +supports-color = "2.1.0" +thiserror = "1.0.50" +tokio = { version = "1.34.0", features = ["rt-multi-thread", "macros"] } +toml = "0.8.8" +uuid = { version = "^1.6.1", features = ["serde"] } bitwarden = { path = "../bitwarden", version = "0.3.1", features = ["secrets"] } [dev-dependencies] -tempfile = "3.5.0" +tempfile = "3.8.1" [target.'cfg(target_os = "linux")'.dependencies] openssl = { version = "0.10", features = ["vendored"] } diff --git a/crates/bws/src/main.rs b/crates/bws/src/main.rs index c7a910260..6f27a0e4d 100644 --- a/crates/bws/src/main.rs +++ b/crates/bws/src/main.rs @@ -324,7 +324,8 @@ async fn process_commands() -> Result<()> { // Load session or return if no session exists let _ = client - .access_token_login(&AccessTokenLoginRequest { access_token }) + .auth() + .login_access_token(&AccessTokenLoginRequest { access_token }) .await?; let organization_id = match client.get_access_token_organization() { diff --git a/crates/bws/src/render.rs b/crates/bws/src/render.rs index 4f2a32e9a..83c5cd532 100644 --- a/crates/bws/src/render.rs +++ b/crates/bws/src/render.rs @@ -9,6 +9,7 @@ use serde::Serialize; pub(crate) enum Output { JSON, YAML, + Env, Table, TSV, None, @@ -49,6 +50,31 @@ pub(crate) fn serialize_response, const N: usiz let text = serde_yaml::to_string(&data).unwrap(); pretty_print("yaml", &text, color); } + Output::Env => { + let valid_key_regex = regex::Regex::new("^[a-zA-Z_][a-zA-Z0-9_]*$").unwrap(); + + let mut commented_out = false; + let mut text: Vec = data + .get_values() + .into_iter() + .map(|row| { + if valid_key_regex.is_match(&row[1]) { + format!("{}=\"{}\"", row[1], row[2]) + } else { + commented_out = true; + format!("# {}=\"{}\"", row[1], row[2].replace('\n', "\n# ")) + } + }) + .collect(); + + if commented_out { + text.push(String::from( + "\n# one or more secrets have been commented-out due to a problematic key name", + )); + } + + pretty_print("sh", &format!("{}\n", text.join("\n")), color); + } Output::Table => { let mut table = Table::new(); table diff --git a/crates/sdk-schemas/Cargo.toml b/crates/sdk-schemas/Cargo.toml index c6de9eaca..2e7706200 100644 --- a/crates/sdk-schemas/Cargo.toml +++ b/crates/sdk-schemas/Cargo.toml @@ -12,10 +12,10 @@ internal = [ ] [dependencies] -schemars = { version = "0.8.12", features = ["preserve_order"] } -serde_json = "1.0.96" -anyhow = "1.0.71" -itertools = "0.11.0" +anyhow = "1.0.75" +itertools = "0.12.0" +schemars = { version = "0.8.16", features = ["preserve_order"] } +serde_json = "1.0.108" bitwarden = { path = "../bitwarden" } bitwarden-json = { path = "../bitwarden-json" } diff --git a/crates/sdk-schemas/src/main.rs b/crates/sdk-schemas/src/main.rs index 9d7e52a47..ef7a2db83 100644 --- a/crates/sdk-schemas/src/main.rs +++ b/crates/sdk-schemas/src/main.rs @@ -99,6 +99,7 @@ fn main() -> Result<()> { write_schema_for_response! { bitwarden::auth::login::ApiKeyLoginResponse, bitwarden::auth::login::PasswordLoginResponse, + bitwarden::auth::login::AccessTokenLoginResponse, bitwarden::secrets_manager::secrets::SecretIdentifiersResponse, bitwarden::secrets_manager::secrets::SecretResponse, bitwarden::secrets_manager::secrets::SecretsResponse, diff --git a/crates/uniffi-bindgen/Cargo.toml b/crates/uniffi-bindgen/Cargo.toml index c9c177c84..cfbb5b554 100644 --- a/crates/uniffi-bindgen/Cargo.toml +++ b/crates/uniffi-bindgen/Cargo.toml @@ -10,4 +10,4 @@ name = "uniffi-bindgen" path = "uniffi-bindgen.rs" [dependencies] -uniffi = { version = "=0.24.1", features = ["cli"] } +uniffi = { version = "=0.25.2", features = ["cli"] } diff --git a/languages/cpp/CMakeBuild.md b/languages/cpp/CMakeBuild.md new file mode 100644 index 000000000..4c7c29814 --- /dev/null +++ b/languages/cpp/CMakeBuild.md @@ -0,0 +1,33 @@ +# CMAKE build + +## INTRODUCTION + +Cmake is used to build the c++ Bitwarden client library. Output should be placed in the build directory. The output contains two dynamic libraries: one that we are building `BitwardenClient` and another that the building library uses `bitwarden_c`. + +## PREREQUISITES + +- Cmake installed, minimum version 3.15 +- `schemas.hpp` generated into `include` directory +- installed `nlohmann-json` library +- installed `boost` library + +## BUILD commands + +One should be in the root directory of the c++ wrapper (the same level where is CMakeLists.txt placed). Paths of the three libraries should be placed inside the cmake build command: + +$ mkdir build +$ cd build +$ cmake .. -DNLOHMANN=/path/to/include/nlohmann -DBOOST=/path/to/include/boost -DTARGET=relative/path/to/libbitwarden_c +$ cmake --build . + + + +## Example + +macOS: + +$ mkdir build +$ cd build +$ cmake .. -DNLOHMANN=/opt/hombrew/include -DBOOST=/opt/homebrew/include -DTARGET=../../target/release/libbitwarden_c.dylib +$ cmake --build . + diff --git a/languages/cpp/CMakeLists.txt b/languages/cpp/CMakeLists.txt new file mode 100644 index 000000000..e513a32ed --- /dev/null +++ b/languages/cpp/CMakeLists.txt @@ -0,0 +1,36 @@ +cmake_minimum_required(VERSION 3.15) +project(BitwardenClient) + +set(CMAKE_CXX_STANDARD 20) + +# Set placeholders to be passed from command line +set(NLOHMANN_JSON_INCLUDE_DIR_PLACEHOLDER ${NLOHMANN}) +set(BOOST_INCLUDE_DIR_PLACEHOLDER ${BOOST}) +set(TARGET_INCLUDE_DIR_PLACEHOLDER ${TARGET}) + +# Specify the locations of nlohmann.json and Boost libraries +find_path(NLOHMANN_JSON_INCLUDE_DIR nlohmann/json.hpp HINTS ${NLOHMANN_JSON_INCLUDE_DIR_PLACEHOLDER}) +find_path(BOOST_INCLUDE_DIR boost/optional.hpp HINTS ${BOOST_INCLUDE_DIR_PLACEHOLDER}) + +# Include directories for library +include_directories(include ${NLOHMANN_JSON_INCLUDE_DIR} ${BOOST_INCLUDE_DIR}) + +# Add library source files +file(GLOB SOURCES "src/*.cpp") + +# Add library source files along with the schemas.cpp file +add_library(BitwardenClient SHARED ${SOURCES} ${SCHEMAS_SOURCE}) + +# Set path for native library loading +set(LIB_BITWARDEN_C "${CMAKE_SOURCE_DIR}/${TARGET}") + +# Copy the library to the build directory before building +add_custom_command( + TARGET BitwardenClient PRE_BUILD + COMMAND ${CMAKE_COMMAND} -E copy + ${LIB_BITWARDEN_C} + $ +) + +# Link libraries +target_link_libraries(BitwardenClient PRIVATE ${LIB_BITWARDEN_C}) diff --git a/languages/cpp/ExampleUse.md b/languages/cpp/ExampleUse.md new file mode 100644 index 000000000..579cd0e67 --- /dev/null +++ b/languages/cpp/ExampleUse.md @@ -0,0 +1,70 @@ +# EXAMPLES + + +## PREREQUISITES + +### BITWARDEN Libraries +One should have two libraries at the same path: +- `BitwardeClient` +- `bitwarden_c` + +It should look like `libBitwardeClient.dylib` and `libbitwarden_c.dylib` for the macOS. + +For Linux: `libBitwardeClient.so` and `libbitwarden_c.so` +For Windows: `BitwardeClient.dll` and `bitwarden_c.dll` + +### INCLUDE directory + +`include` directory contains: +- `BitwardenLibrary.h` +- `BitwardenClient.h` +- `BitwardenSettings.h` +- `CommandRunner.h` +- `Projects.h` +- `Secrets.h` +- `schemas.hpp` + +### Other libraries +- `nlohmann-json` (https://github.com/nlohmann/json) +- `boost` (https://www.boost.org/) + + +### COMPILING + +One could use g++/clang++ for compiling. +Example of the folder structure (macOS): + +--root + --build + `libBitwardenClient.dylib` + `libbitwarden_c.dylib` + --include + --`BitwardenLibrary.h` + --`BitwardenClient.h` + --`BitwardenSettings.h` + --`CommandRunner.h` + --`Projects.h` + --`Secrets.h` + --`schemas.hpp` + --examples + --`Wrapper.cpp` + + +1. $ export ACCESS_TOKEN=<"access-token"> +2. $ export ORGANIZATION_ID=<"organization-id"> +3. $ export DYLD_LIBRARY_PATH=/path/to/your/library:$DYLD_LIBRARY_PATH + +The last step is neccessary to add the path for the dynamic library (macOS). +For the Linux one should use: +$ export LD_LIBRARY_PATH=/path/to/your/library:$LD_LIBRARY_PATH +For the Windows: +$ set PATH=%PATH%;C:\path\to\your\library + +4. $ cd examples +5. $ clang++ -std=c++20 -I../include -I/path/to/include/nlohmann -I/path/to/include/boost -L../build/ -o MyBitwardenApp Wrapper.cpp -lBitwardenClient -ldl + +for Windows `-ldl` should be excluded, + +The result is `MyBitwardenApp` in the `examples` directory, and one can run it from the `examples` directory: + +6. $ ./MyBitwardenApp diff --git a/languages/cpp/README.md b/languages/cpp/README.md new file mode 100644 index 000000000..23b59ac76 --- /dev/null +++ b/languages/cpp/README.md @@ -0,0 +1,97 @@ +# Bitwarden Secrets Manager SDK + +C++ bindings for interacting with the [Bitwarden Secrets Manager]. This is a beta release and might be missing some functionality. + +## Create access token + +Review the help documentation on [Access Tokens] + +## Usage code snippets + +### Client settings + +```c++ +// Optional - if not stressed, then default values are used +BitwardenSettings bitwardenSettings; +bitwardenSettings.set_api_url(""); +bitwardenSettings.set_identity_url(""); +``` + + +### Create new Bitwarden client + +```c++ +std::string accessToken = ""; +// Optional - argument in BitwardenClient +BitwardenClient bitwardenClient = BitwardenClient(bitwardenSettings); +bitwardenClient.accessTokenLogin(accessToken); +``` + +### Create new project + +```c++ +boost::uuids::uuid organizationUuid = boost::uuids::string_generator()(""); +ProjectResponse projectResponseCreate = bitwardenClient.createProject(organizationUuid, "TestProject"); +``` + +### List all projects + +```c++ +ProjectsResponse projectResponseList = bitwardenClient.listProjects(organizationUuid); +``` + +### Get project details + +```c++ +boost::uuids::uuid projectId = boost::uuids::string_generator()(projectResponseCreate.get_id()); +ProjectResponse projectResponseGet = bitwardenClient.getProject(projectId); +``` + +### Update project + +```c++ +boost::uuids::uuid projectId = boost::uuids::string_generator()(projectResponseCreate.get_id()); +ProjectResponse projectResponseUpdate = bitwardenClient.updateProject(projectId, organizationUuid, "TestProjectUpdated"); +``` + +### Delete projects + +```c++ +SecretsDeleteResponse secretsDeleteResponse = bitwardenClient.deleteSecrets({secretId}); +``` + +### Add new secret + +```c++ +std::string key = "key"; +std::string value = "value"; +std::string note = "note"; +SecretResponse secretResponseCreate = bitwardenClient.createSecret(key, value, note, organizationUuid, {projectId}); +``` + +### List secrets + +```c++ +SecretIdentifiersResponse secretIdentifiersResponse = bitwardenClient.listSecrets(organizationUuid); +``` + +### Get secret details + +``` +boost::uuids::uuid secretId = boost::uuids::string_generator()(secretResponseCreate.get_id()); +SecretResponse secretResponseGet = bitwardenClient.getSecret(secretId); +``` + +### Update secret +```c++ +SecretResponse secretResponseUpdate = bitwardenClient.updateSecret(secretId, "key2", "value2", "note2", organizationUuid, {projectId}); +``` + +# Delete secrets + +```c++ +SecretsDeleteResponse secretsDeleteResponse = bitwardenClient.deleteSecrets({secretId}); +``` + +[Access Tokens]: https://bitwarden.com/help/access-tokens/ +[Bitwarden Secrets Manager]: https://bitwarden.com/products/secrets-manager/ diff --git a/languages/cpp/examples/Wrapper.cpp b/languages/cpp/examples/Wrapper.cpp new file mode 100644 index 000000000..df4aa164c --- /dev/null +++ b/languages/cpp/examples/Wrapper.cpp @@ -0,0 +1,73 @@ +#include "BitwardenClient.h" +#include +#include + +int main() { + // Retrieve access token and organization ID from environment variables + const char* accessTokenEnv = std::getenv("ACCESS_TOKEN"); + const char* organizationIdEnv = std::getenv("ORGANIZATION_ID"); + + if (!accessTokenEnv || !organizationIdEnv) { + std::cerr << "Error: Environment variables ACCESS_TOKEN or ORGANIZATION_ID not set." << std::endl; + return 1; + } + + std::string accessToken = accessTokenEnv; + std::string organizationId = organizationIdEnv; + + + + // Optional - commented to use default values + // BitwardenSettings bitwardenSettings; + // bitwardenSettings.set_api_url(""); + // bitwardenSettings.set_identity_url(""); + + // Create a Bitwarden client instance + BitwardenClient bitwardenClient = BitwardenClient(); + // // Access token login + bitwardenClient.accessTokenLogin(accessToken); + // Organization ID + boost::uuids::uuid organizationUuid = boost::uuids::string_generator()(organizationId); + + // // Create a new project + ProjectResponse projectResponseCreate = bitwardenClient.createProject(organizationUuid, "NewTestProject"); + boost::uuids::uuid projectId = boost::uuids::string_generator()(projectResponseCreate.get_id()); + + // List projects + ProjectsResponse projectResponseList = bitwardenClient.listProjects(organizationUuid); + + // Get project details + ProjectResponse projectResponseGet = bitwardenClient.getProject(projectId); + + // Update project + ProjectResponse ProjectResponseUpdate = bitwardenClient.updateProject(projectId, organizationUuid, "NewTestProject2"); + + // Secrets + std::string key = "key"; + std::string value = "value"; + std::string note = "note"; + + // Create a new secret + SecretResponse secretResponseCreate = bitwardenClient.createSecret(key, value, note, organizationUuid, {projectId}); + boost::uuids::uuid secretId = boost::uuids::string_generator()(secretResponseCreate.get_id()); + + // List secrets + SecretIdentifiersResponse secretIdentifiersResponse = bitwardenClient.listSecrets(organizationUuid); + + // Get secret details + SecretResponse secretResponseGet = bitwardenClient.getSecret(secretId); + + // Update secret + key = "key2"; + value = "value2"; + note = "note2"; + SecretResponse responseForSecretResponseUpdate = bitwardenClient.updateSecret(secretId, key, value, note, organizationUuid, {projectId}); + + // Delete secrets + SecretsDeleteResponse secretsDeleteResponse = bitwardenClient.deleteSecrets({secretId}); + + // Delete projects + ProjectsDeleteResponse projectsDeleteResponse = bitwardenClient.deleteProjects({projectId}); + + return 0; +} diff --git a/languages/cpp/include/BitwardenClient.h b/languages/cpp/include/BitwardenClient.h new file mode 100644 index 000000000..a5cf72475 --- /dev/null +++ b/languages/cpp/include/BitwardenClient.h @@ -0,0 +1,36 @@ +#pragma once + +#include "CommandRunner.h" +#include "BitwardenSettings.h" +#include "Projects.h" +#include "Secrets.h" +#include +#include + +class BitwardenClient { +public: + BitwardenClient(const BitwardenSettings& bitwardenSettings = BitwardenSettings()); + ~BitwardenClient(); + + void accessTokenLogin(const std::string& accessToken); + ProjectResponse getProject(const boost::uuids::uuid& id); + ProjectResponse createProject(const boost::uuids::uuid& organizationId, const std::string& name); + ProjectResponse updateProject(const boost::uuids::uuid& id, const boost::uuids::uuid& organizationId, const std::string& name); + ProjectsDeleteResponse deleteProjects(const std::vector& ids); + ProjectsResponse listProjects(const boost::uuids::uuid &organizationId); + SecretResponse getSecret(const boost::uuids::uuid& id); + SecretResponse createSecret(const std::string& key, const std::string& value, const std::string& note, const boost::uuids::uuid& organizationId, const std::vector& projectIds); + SecretResponse updateSecret(const boost::uuids::uuid& id, const std::string& key, const std::string& value, const std::string& note, const boost::uuids::uuid& organizationId, const std::vector& projectIds); + SecretsDeleteResponse deleteSecrets(const std::vector& ids); + SecretIdentifiersResponse listSecrets(const boost::uuids::uuid& organizationId); + +private: + BitwardenLibrary* library; + void* client; + CommandRunner* commandRunner; + Projects projects; + Secrets secrets; + bool isClientOpen; + ClientSettings clientSettings; + +}; diff --git a/languages/cpp/include/BitwardenLibrary.h b/languages/cpp/include/BitwardenLibrary.h new file mode 100644 index 000000000..5fee78726 --- /dev/null +++ b/languages/cpp/include/BitwardenLibrary.h @@ -0,0 +1,27 @@ +#pragma once + +#include + +#ifdef _WIN32 +#include +#else +#include +#endif + +class BitwardenLibrary { +public: + BitwardenLibrary(const std::string& providedLibraryPath); + ~BitwardenLibrary(); + + void* init(const char* clientSettingsJson); + void free_mem(void* client); + const char* run_command(const char* commandJson, void* client); + +private: +#ifdef _WIN32 + HMODULE libraryHandle; +#else + void* libraryHandle; +#endif +}; + diff --git a/languages/cpp/include/BitwardenSettings.h b/languages/cpp/include/BitwardenSettings.h new file mode 100644 index 000000000..4d075ed0a --- /dev/null +++ b/languages/cpp/include/BitwardenSettings.h @@ -0,0 +1,19 @@ +#pragma once + +#include + +class BitwardenSettings { +public: + BitwardenSettings() = default; + ~BitwardenSettings() = default; + + const std::string& get_api_url() const { return api_url; } + void set_api_url(const std::string& value) { api_url = value; } + + const std::string& get_identity_url() const { return identity_url; } + void set_identity_url(const std::string& value) { identity_url = value; } + +private: + std::string api_url; + std::string identity_url; +}; diff --git a/languages/cpp/include/CommandRunner.h b/languages/cpp/include/CommandRunner.h new file mode 100644 index 000000000..9aa6cbe9c --- /dev/null +++ b/languages/cpp/include/CommandRunner.h @@ -0,0 +1,45 @@ +#pragma once + +#include +#include +#include "BitwardenLibrary.h" +#include "schemas.hpp" +#include + +using namespace Bitwarden::Sdk; + +class CommandRunner { +public: + CommandRunner(BitwardenLibrary* library, void* client); + + template + R runCommand(const Command& command, Func deserializer); + + + +private: + BitwardenLibrary* library; + void* client; + + std::string commandToString(const Command& command); + nlohmann::json filterNullObjects(const nlohmann::json& input); +}; + +template +R CommandRunner::runCommand(const Command& command, Func deserializer) { + // Serialize the Command object to a JSON string + std::string jsonString = commandToString(command); + const char* jsonCStr = jsonString.c_str(); + const char* response = library->run_command(jsonCStr, client); + + // Deserialize the response using the provided deserializer function + T deserialized = deserializer(response); + + // Unwrap the response and throw an exception if it was not successful + if (!deserialized.get_success()) { + throw std::runtime_error(*deserialized.get_error_message()); + } + + return deserialized.get_data().get(); +} + diff --git a/languages/cpp/include/Projects.h b/languages/cpp/include/Projects.h new file mode 100644 index 000000000..9bef19b9c --- /dev/null +++ b/languages/cpp/include/Projects.h @@ -0,0 +1,19 @@ +#pragma once + +#include +#include +#include "CommandRunner.h" + +class Projects { +public: + Projects(CommandRunner* commandRunner); + + ProjectResponse get(const boost::uuids::uuid& id); + ProjectResponse create(const boost::uuids::uuid& organizationId, const std::string& name); + ProjectResponse update(const boost::uuids::uuid& id, const boost::uuids::uuid& organizationId, const std::string& name); + ProjectsDeleteResponse deleteProjects(const std::vector& ids); + ProjectsResponse list(const boost::uuids::uuid& organizationId); + +private: + CommandRunner* commandRunner; +}; diff --git a/languages/cpp/include/Secrets.h b/languages/cpp/include/Secrets.h new file mode 100644 index 000000000..024ec3692 --- /dev/null +++ b/languages/cpp/include/Secrets.h @@ -0,0 +1,20 @@ +#pragma once + +#include +#include +#include "CommandRunner.h" + +class Secrets { +public: + Secrets(CommandRunner* commandRunner); + + SecretResponse get(const boost::uuids::uuid& id); + SecretResponse create(const std::string& key, const std::string& value, const std::string& note, const boost::uuids::uuid& organizationId, const std::vector& projectIds); + SecretResponse update(const boost::uuids::uuid& id, const std::string& key, const std::string& value, const std::string& note, const boost::uuids::uuid& organizationId, const std::vector& projectIds); + SecretsDeleteResponse deleteSecrets(const std::vector& ids); + SecretIdentifiersResponse list(const boost::uuids::uuid& organizationId); + +private: + CommandRunner* commandRunner; +}; + diff --git a/languages/cpp/src/BitwardenClient.cpp b/languages/cpp/src/BitwardenClient.cpp new file mode 100644 index 000000000..fef9ea267 --- /dev/null +++ b/languages/cpp/src/BitwardenClient.cpp @@ -0,0 +1,145 @@ +#include "BitwardenClient.h" +#include +#include + +BitwardenClient::BitwardenClient(const BitwardenSettings& bitwardenSettings) + : library(nullptr), commandRunner(nullptr), isClientOpen(false), projects(nullptr), secrets(nullptr) { + + // Set default values for optional strings + boost::optional apiUrl = bitwardenSettings.get_api_url().empty() + ? boost::optional("https://api.bitwarden.com") + : boost::optional(bitwardenSettings.get_api_url()); + + boost::optional identityUrl = bitwardenSettings.get_identity_url().empty() + ? boost::optional("https://identity.bitwarden.com") + : boost::optional(bitwardenSettings.get_identity_url()); + + boost::optional user_agent = boost::optional("Bitwarden CPP-SDK"); + + // Set values in clientSettings + clientSettings.set_device_type(Bitwarden::Sdk::DeviceType::SDK); + clientSettings.set_user_agent(user_agent); + clientSettings.set_api_url(apiUrl); + clientSettings.set_identity_url(identityUrl); + + nlohmann::json jsonClientSettings; + Bitwarden::Sdk::to_json(jsonClientSettings, clientSettings); + + std::string jsonClientSettingsString = jsonClientSettings.dump(); + const char* jsonClientSettingsCStr = jsonClientSettingsString.c_str(); + + try { + library = new BitwardenLibrary("./"); + client = library->init(jsonClientSettingsCStr); + commandRunner = new CommandRunner(library, client); + projects = Projects(commandRunner); + secrets = Secrets(commandRunner); + isClientOpen = true; + } catch (const std::exception& ex) { + std::cerr << "Failed to initialize: " << ex.what() << std::endl; + throw ex; + } +} + +BitwardenClient::~BitwardenClient() { + if (library) { + delete commandRunner; + library->free_mem(client); + delete library; + isClientOpen = false; + } +} + +void BitwardenClient::accessTokenLogin(const std::string& accessToken) { + Command command; + AccessTokenLoginRequest accessTokenLoginRequest; + accessTokenLoginRequest.set_access_token(accessToken); + command.set_access_token_login(accessTokenLoginRequest); + + auto deserializer = [](const char* response) -> ResponseForApiKeyLoginResponse { + nlohmann::json jsonResponse = nlohmann::json::parse(response); + ResponseForApiKeyLoginResponse loginResponse; + Bitwarden::Sdk::from_json(jsonResponse, loginResponse); + return loginResponse; + }; + try { + commandRunner->runCommand(command, deserializer); + } catch (const std::exception& ex) { + std::cerr << "Error in accessTokenLogin: " << ex.what() << std::endl; + throw ex; + } +} + +ProjectResponse BitwardenClient::getProject(const boost::uuids::uuid& id){ + if (!isClientOpen) { + throw std::runtime_error("Client is not open."); + } + return projects.get(id); +} + +ProjectResponse BitwardenClient::createProject(const boost::uuids::uuid& organizationId, const std::string& name){ + if (!isClientOpen) { + throw std::runtime_error("Client is not open."); + } + return projects.create(organizationId, name); +} + +ProjectResponse BitwardenClient::updateProject(const boost::uuids::uuid& id, const boost::uuids::uuid& organizationId, const std::string& name){ + if (!isClientOpen) { + throw std::runtime_error("Client is not open."); + } + return projects.update(id, organizationId, name); +} + +ProjectsDeleteResponse BitwardenClient::deleteProjects(const std::vector& ids) { + if (!isClientOpen) { + throw std::runtime_error("Client is not open."); + } + return projects.deleteProjects(ids); + +} + +ProjectsResponse BitwardenClient::listProjects(const boost::uuids::uuid &organizationId) { + if (!isClientOpen) { + throw std::runtime_error("Client is not open."); + } + return projects.list(organizationId); + +} + +SecretResponse BitwardenClient::getSecret(const boost::uuids::uuid& id){ + if (!isClientOpen) { + throw std::runtime_error("Client is not open."); + } + return secrets.get(id); +} + +SecretResponse BitwardenClient::createSecret(const std::string& key, const std::string& value, const std::string& note, const boost::uuids::uuid& organizationId, const std::vector& projectIds){ + if (!isClientOpen) { + throw std::runtime_error("Client is not open."); + } + return secrets.create(key, value, note, organizationId, projectIds); +} + +SecretResponse BitwardenClient::updateSecret(const boost::uuids::uuid& id, const std::string& key, const std::string& value, const std::string& note, const boost::uuids::uuid& organizationId, const std::vector& projectIds){ + if (!isClientOpen) { + throw std::runtime_error("Client is not open."); + } + return secrets.update(id, key, value, note, organizationId, projectIds); +} + +SecretsDeleteResponse BitwardenClient::deleteSecrets(const std::vector& ids) { + if (!isClientOpen) { + throw std::runtime_error("Client is not open."); + } + return secrets.deleteSecrets(ids); + +} + +SecretIdentifiersResponse BitwardenClient::listSecrets(const boost::uuids::uuid &organizationId) { + if (!isClientOpen) { + throw std::runtime_error("Client is not open."); + } + return secrets.list(organizationId); + +} diff --git a/languages/cpp/src/BitwardenLibrary.cpp b/languages/cpp/src/BitwardenLibrary.cpp new file mode 100644 index 000000000..0af592786 --- /dev/null +++ b/languages/cpp/src/BitwardenLibrary.cpp @@ -0,0 +1,107 @@ +#include "BitwardenLibrary.h" +#include + +BitwardenLibrary::BitwardenLibrary(const std::string& providedLibraryPath) : libraryHandle(nullptr) { + std::string libraryExtension; + std::string libraryNameUnix = "libbitwarden_c"; + std::string libraryNameWin = "bitwarden_c"; +#if defined(_WIN32) + libraryExtension = ".dll"; +#elif defined(__linux__) + libraryExtension = ".so"; +#elif defined(__APPLE__) + libraryExtension = ".dylib"; +#else + // Unsupported platform + std::cerr << "Unsupported platform." << std::endl; + return; +#endif + + // Load the dynamic library +#ifdef _WIN32 + std::string libraryPath = providedLibraryPath + libraryNameWin + libraryExtension; + // Load the dynamic library on Windows + libraryHandle = LoadLibraryA(libraryPath.c_str()); + + if (!libraryHandle) { + std::cerr << "Failed to load the Bitwarden library." << std::endl; + } +#else + std::string libraryPath = providedLibraryPath + libraryNameUnix + libraryExtension; + // Load the dynamic library on Unix-based systems (Linux, macOS) + libraryHandle = dlopen(libraryPath.c_str(), RTLD_NOW); + + if (!libraryHandle) { + std::cerr << "Failed to load the Bitwarden library: " << dlerror() << std::endl; + } +#endif +} + +BitwardenLibrary::~BitwardenLibrary() { + if (libraryHandle) { +#ifdef _WIN32 + FreeLibrary(libraryHandle); +#else + dlclose(libraryHandle); +#endif + } +} + +void* BitwardenLibrary::init(const char* clientSettingsJson) { + typedef void* (*InitFunction)(const char*); + InitFunction initFunction = nullptr; + +#ifdef _WIN32 + // Get the address of the init function on Windows + initFunction = reinterpret_cast(GetProcAddress(libraryHandle, "init")); +#else + // Get the address of the init function on Unix-based systems + initFunction = reinterpret_cast(dlsym(libraryHandle, "init")); +#endif + + if (initFunction) { + return initFunction(clientSettingsJson); + } + + std::cerr << "Failed to load init function from the Bitwarden library: " << std::endl; + return nullptr; +} + +void BitwardenLibrary::free_mem(void* client) { + typedef void (*FreeMemFunction)(void*); + FreeMemFunction freeMemFunction = nullptr; + +#ifdef _WIN32 + // Get the address of the free_mem function on Windows + freeMemFunction = reinterpret_cast(GetProcAddress(libraryHandle, "free_mem")); +#else + // Get the address of the free_mem function on Unix-based systems + freeMemFunction = reinterpret_cast(dlsym(libraryHandle, "free_mem")); +#endif + + if (freeMemFunction) { + freeMemFunction(client); + } else { + std::cerr << "Failed to load free_mem function from the Bitwarden library." << std::endl; + } +} + +const char* BitwardenLibrary::run_command(const char* commandJson, void* client) { + typedef const char* (*RunCommandFunction)(const char*, void*); + RunCommandFunction runCommandFunction = nullptr; + +#ifdef _WIN32 + // Get the address of the run_command function on Windows + runCommandFunction = reinterpret_cast(GetProcAddress(libraryHandle, "run_command")); +#else + // Get the address of the run_command function on Unix-based systems + runCommandFunction = reinterpret_cast(dlsym(libraryHandle, "run_command")); +#endif + + if (runCommandFunction) { + return runCommandFunction(commandJson, client); + } + + std::cerr << "Failed to load run_command function from the Bitwarden library." << std::endl; + return nullptr; +} diff --git a/languages/cpp/src/CommandRunner.cpp b/languages/cpp/src/CommandRunner.cpp new file mode 100644 index 000000000..032347f34 --- /dev/null +++ b/languages/cpp/src/CommandRunner.cpp @@ -0,0 +1,49 @@ +#include "CommandRunner.h" +#include +#include +#include + + +CommandRunner::CommandRunner(BitwardenLibrary* library, void* client) : library(library), client(client) {} + +// Function to recursively filter out objects with all null values +nlohmann::json CommandRunner::filterNullObjects(const nlohmann::json& input) { + nlohmann::json result; + + for (auto it = input.begin(); it != input.end(); ++it) { + if (!it.value().is_null()) { + if (it.value().is_object()) { + // Recursively filter nested objects + json nestedFiltered = filterNullObjects(it.value()); + if (!nestedFiltered.empty()) { + result[it.key()] = nestedFiltered; + } + } else { + result[it.key()] = it.value(); + } + } + } + + return result; +} + +// Implement the commandToString function +std::string CommandRunner::commandToString(const Command& command) { + try { + // Create an nlohmann::json object from the Command object + nlohmann::json jsonCommand; + nlohmann::json filteredJsonCommand; + + Bitwarden::Sdk::to_json(jsonCommand, command); + + filteredJsonCommand = filterNullObjects(jsonCommand); + + // Convert the JSON to a string + std::string jsonCommandString = filteredJsonCommand.dump(); + + return jsonCommandString; + } catch (const std::exception& ex) { + std::cerr << "Error: " << ex.what() << std::endl; + throw ex; + } +} diff --git a/languages/cpp/src/Projects.cpp b/languages/cpp/src/Projects.cpp new file mode 100644 index 000000000..d0aa6ed49 --- /dev/null +++ b/languages/cpp/src/Projects.cpp @@ -0,0 +1,132 @@ +#include "Projects.h" +#include +#include +#include +#include +#include + +Projects::Projects(CommandRunner* commandRunner) : commandRunner(commandRunner) {} + +auto projectsDeserializer = [](const char* response) -> ResponseForProjectResponse { + nlohmann::json jsonResponse = nlohmann::json::parse(response); + ResponseForProjectResponse projectResponse; + Bitwarden::Sdk::from_json(jsonResponse, projectResponse); + return projectResponse; +}; + +auto deleteProjectsDeserializer = [](const char* response) -> ResponseForProjectsDeleteResponse { + nlohmann::json jsonResponse = nlohmann::json::parse(response); + ResponseForProjectsDeleteResponse deleteProjectsResponse; + Bitwarden::Sdk::from_json(jsonResponse, deleteProjectsResponse); + return deleteProjectsResponse; +}; + +auto projectListDeserializer = [](const char* response) -> ResponseForProjectsResponse { + nlohmann::json jsonResponse = nlohmann::json::parse(response); + ResponseForProjectsResponse listResponse; + Bitwarden::Sdk::from_json(jsonResponse, listResponse); + return listResponse; +}; + +ProjectResponse Projects::get(const boost::uuids::uuid& id) { + Command command; + ProjectsCommand projectsCommand; + ProjectGetRequest projectGetRequest; + + std::string idStr = boost::uuids::to_string(id); + projectGetRequest.set_id(idStr); + + projectsCommand.set_get(projectGetRequest); + command.set_projects(projectsCommand); + + try { + return commandRunner->runCommand(command, projectsDeserializer); + } catch (const std::exception& ex) { + std::cerr << "Error in getProject: " << ex.what() << std::endl; + throw ex; + } +} + +ProjectResponse Projects::create(const boost::uuids::uuid& organizationId, const std::string& name) { + Command command; + ProjectsCommand projectsCommand; + ProjectCreateRequest projectCreateRequest; + + std::string orgIdStr = boost::uuids::to_string(organizationId); + projectCreateRequest.set_organization_id(orgIdStr); + + projectCreateRequest.set_name(name); + projectsCommand.set_create(projectCreateRequest); + command.set_projects(projectsCommand); + + try { + return commandRunner->runCommand(command, projectsDeserializer); + } catch (const std::exception& ex) { + std::cerr << "Error in createProject: " << ex.what() << std::endl; + throw ex; + } +} + +ProjectResponse Projects::update(const boost::uuids::uuid& id, const boost::uuids::uuid& organizationId, const std::string& name) { + Command command; + ProjectsCommand projectsCommand; + ProjectPutRequest projectPutRequest; + + std::string idStr = boost::uuids::to_string(id); + projectPutRequest.set_id(idStr); + + std::string orgIdStr = boost::uuids::to_string(organizationId); + projectPutRequest.set_organization_id(orgIdStr); + + projectPutRequest.set_name(name); + projectsCommand.set_update(projectPutRequest); + command.set_projects(projectsCommand); + + try { + return commandRunner->runCommand(command, projectsDeserializer); + } catch (const std::exception& ex) { + std::cerr << "Error in updateProject: " << ex.what() << std::endl; + throw ex; + } +} + +ProjectsDeleteResponse Projects::deleteProjects(const std::vector& ids) { + Command command; + ProjectsCommand projectsCommand; + ProjectsDeleteRequest projectsDeleteRequest; + + std::vector idStrs; + for (const auto& id : ids) { + idStrs.push_back(boost::uuids::to_string(id)); + } + projectsDeleteRequest.set_ids(idStrs); + + projectsCommand.set_projects_command_delete(projectsDeleteRequest); + command.set_projects(projectsCommand); + + try { + return commandRunner->runCommand(command, deleteProjectsDeserializer); + } catch (const std::exception& ex) { + std::cerr << "Error in deleteProjects: " << ex.what() << std::endl; + throw ex; + } +} + +ProjectsResponse Projects::list(const boost::uuids::uuid& organizationId) { + Command command; + ProjectsCommand projectsCommand; + ProjectsListRequest projectsListRequest; + + std::string orgIdStr = boost::uuids::to_string(organizationId); + projectsListRequest.set_organization_id(orgIdStr); + + projectsCommand.set_list(projectsListRequest); + command.set_projects(projectsCommand); + + try { + return commandRunner->runCommand(command, projectListDeserializer); + } catch (const std::exception& ex) { + std::cerr << "Error in listProjects: " << ex.what() << std::endl; + throw ex; + } +} diff --git a/languages/cpp/src/Secrets.cpp b/languages/cpp/src/Secrets.cpp new file mode 100644 index 000000000..e153ea7f1 --- /dev/null +++ b/languages/cpp/src/Secrets.cpp @@ -0,0 +1,149 @@ +#include "Secrets.h" +#include +#include +#include +#include + +Secrets::Secrets(CommandRunner* commandRunner) : commandRunner(commandRunner) {} + +auto secretsDeserializer = [](const std::string& response) -> ResponseForSecretResponse { + nlohmann::json jsonResponse = nlohmann::json::parse(response); + ResponseForSecretResponse secretResponse; + Bitwarden::Sdk::from_json(jsonResponse, secretResponse); + return secretResponse; +}; + +auto deleteSecretsDeserializer = [](const std::string& response) -> ResponseForSecretsDeleteResponse { + nlohmann::json jsonResponse = nlohmann::json::parse(response); + ResponseForSecretsDeleteResponse deleteSecretsResponse; + Bitwarden::Sdk::from_json(jsonResponse, deleteSecretsResponse); + return deleteSecretsResponse; +}; + +auto secretListDeserializer = [](const std::string& response) -> ResponseForSecretIdentifiersResponse { + nlohmann::json jsonResponse = nlohmann::json::parse(response); + ResponseForSecretIdentifiersResponse listResponse; + Bitwarden::Sdk::from_json(jsonResponse, listResponse); + return listResponse; +}; + +SecretResponse Secrets::get(const boost::uuids::uuid& id) { + Command command; + SecretsCommand secretsCommand; + SecretGetRequest secretGetRequest; + + std::string idStr = boost::uuids::to_string(id); + secretGetRequest.set_id(idStr); + + secretsCommand.set_get(secretGetRequest); + command.set_secrets(secretsCommand); + + try { + return commandRunner->runCommand(command, secretsDeserializer); + } catch (const std::exception& ex) { + std::cerr << "Error in getSecret: " << ex.what() << std::endl; + throw ex; + } +} + +SecretResponse Secrets::create(const std::string& key, const std::string& value, const std::string& note, const boost::uuids::uuid& organizationId, const std::vector& projectIds) { + Command command; + SecretsCommand secretsCommand; + SecretCreateRequest secretCreateRequest; + + std::string orgIdStr = boost::uuids::to_string(organizationId); + secretCreateRequest.set_organization_id(orgIdStr); + + secretCreateRequest.set_key(key); + secretCreateRequest.set_value(value); + secretCreateRequest.set_note(note); + + std::vector projectIdsStr; + for (const auto& projectId : projectIds) { + projectIdsStr.push_back(boost::uuids::to_string(projectId)); + } + secretCreateRequest.set_project_ids(projectIdsStr); + + secretsCommand.set_create(secretCreateRequest); + command.set_secrets(secretsCommand); + + try { + return commandRunner->runCommand(command, secretsDeserializer); + } catch (const std::exception& ex) { + std::cerr << "Error in createSecret: " << ex.what() << std::endl; + throw ex; + } +} + +SecretResponse Secrets::update(const boost::uuids::uuid& id, const std::string& key, const std::string& value, const std::string& note, const boost::uuids::uuid& organizationId, const std::vector& projectIds) { + Command command; + SecretsCommand secretsCommand; + SecretPutRequest secretPutRequest; + + std::string idStr = boost::uuids::to_string(id); + secretPutRequest.set_id(idStr); + + std::string orgIdStr = boost::uuids::to_string(organizationId); + secretPutRequest.set_organization_id(orgIdStr); + + secretPutRequest.set_key(key); + secretPutRequest.set_value(value); + secretPutRequest.set_note(note); + + std::vector projectIdsStr; + for (const auto& projectId : projectIds) { + projectIdsStr.push_back(boost::uuids::to_string(projectId)); + } + secretPutRequest.set_project_ids(projectIdsStr); + + secretsCommand.set_update(secretPutRequest); + command.set_secrets(secretsCommand); + + try { + return commandRunner->runCommand(command, secretsDeserializer); + } catch (const std::exception& ex) { + std::cerr << "Error in updateSecret: " << ex.what() << std::endl; + throw ex; + } +} + +SecretsDeleteResponse Secrets::deleteSecrets(const std::vector& ids) { + Command command; + SecretsCommand secretsCommand; + SecretsDeleteRequest secretsDeleteRequest; + + std::vector idsStr; + for (const auto& id : ids) { + idsStr.push_back(boost::uuids::to_string(id)); + } + secretsDeleteRequest.set_ids(idsStr); + + secretsCommand.set_secrets_command_delete(secretsDeleteRequest); + command.set_secrets(secretsCommand); + + try { + return commandRunner->runCommand(command, deleteSecretsDeserializer); + } catch (const std::exception& ex) { + std::cerr << "Error in deleteSecrets: " << ex.what() << std::endl; + throw ex; + } +} + +SecretIdentifiersResponse Secrets::list(const boost::uuids::uuid& organizationId) { + Command command; + SecretsCommand secretsCommand; + SecretIdentifiersRequest secretIdentifiersRequest; + + std::string orgIdStr = boost::uuids::to_string(organizationId); + secretIdentifiersRequest.set_organization_id(orgIdStr); + + secretsCommand.set_list(secretIdentifiersRequest); + command.set_secrets(secretsCommand); + + try { + return commandRunner->runCommand(command, secretListDeserializer); + } catch (const std::exception& ex) { + std::cerr << "Error in listSecret: " << ex.what() << std::endl; + throw ex; + } +} diff --git a/languages/csharp/global.json b/languages/csharp/global.json index 527fd31d3..10b65be86 100644 --- a/languages/csharp/global.json +++ b/languages/csharp/global.json @@ -1,6 +1,6 @@ { "sdk": { - "version": "6.0.413", + "version": "6.0.100", "rollForward": "latestFeature" } } diff --git a/languages/go/README.md b/languages/go/README.md new file mode 100644 index 000000000..e57badf7e --- /dev/null +++ b/languages/go/README.md @@ -0,0 +1,113 @@ +# Bitwarden SDK in Go + +This SDK is designed to interact with Bitwarden services in Go. It includes implementations for +managing projects and secrets, as well as a client interface to facilitate operations like login. + +## Prerequisites + +- Go installed +- C environment to run CGO + +## Installation + +Download the SDK files and place them in your Go project directory. + +## Table of Contents + +- [Initialization](#initialization) +- [Login](#login) +- [Projects](#projects) +- [Secrets](#secrets) +- [Close Client](#close-client) + +--- + +### Initialization + +To initialize the client, you need to import the SDK and create a new `BitwardenClient` instance. + +```go +import "github.com/bitwarden/sdk/languages/go" + +bitwardenClient, _ := sdk.NewBitwardenClient(&apiURL, &identityURL) +``` + +--- + +### Login + +To login using an access token: + +```go +apiKeyLogin, err := bitwardenClient.AccessTokenLogin(accessToken) +``` + +--- + +### Projects + +#### Create a Project + +```go +project, err := client.Projects.Create("organization_id", "project_name") +``` + +#### List Projects + +```go +projects, err := client.Projects.List("organization_id") +``` + +#### Update a Project + +```go +project, err := client.Projects.Update("project_id", "organization_id", "new_project_name") +``` + +#### Delete Projects + +```go +project, err := client.Projects.Delete([]string{"project_id_1", "project_id_2"}) +``` + +--- + +### Secrets + +#### Create a Secret + +```go +secret, err := client.Secrets.Create("key", "value", "note", "organization_id", []string{"project_id"}) +``` + +#### List Secrets + +```go +secrets, err := client.Secrets.List("organization_id") +``` + +#### Update a Secret + +```go +secret, err := client.Secrets.Update("secret_id", "new_key", "new_value", "new_note", "organization_id", []string{"project_id"}) +``` + +#### Delete Secrets + +```go +secret, err := client.Secrets.Delete([]string{"secret_id_1", "secret_id_2"}) +``` + +--- + +### Close Client + +To free up resources: + +```go +defer bitwardenClient.Close() +``` + +--- + +For more detailed information, refer to the code comments and method signatures. diff --git a/languages/go/bitwarden_client.go b/languages/go/bitwarden_client.go new file mode 100644 index 000000000..5e1108ce1 --- /dev/null +++ b/languages/go/bitwarden_client.go @@ -0,0 +1,63 @@ +package sdk + +import ( + "encoding/json" + + "github.com/bitwarden/sdk/languages/go/internal/cinterface" +) + +type BitwardenClient struct { + client cinterface.ClientPointer + lib cinterface.BitwardenLibrary + commandRunner CommandRunnerInterface + Projects ProjectsInterface + Secrets SecretsInterface +} + +func NewBitwardenClient(apiURL *string, identityURL *string) (*BitwardenClient, error) { + deviceType := DeviceType("SDK") + userAgent := "Bitwarden GOLANG-SDK" + clientSettings := ClientSettings{ + APIURL: apiURL, + IdentityURL: identityURL, + UserAgent: &userAgent, + DeviceType: &deviceType, + } + + settingsJSON, err := json.Marshal(clientSettings) + if err != nil { + return nil, err + } + + lib := cinterface.NewBitwardenLibrary() + client, err := lib.Init(string(settingsJSON)) + if err != nil { + return nil, err + } + runner := NewCommandRunner(client, lib) + + return &BitwardenClient{ + lib: lib, + client: client, + commandRunner: runner, + Projects: NewProjects(runner), + Secrets: NewSecrets(runner), + }, nil +} + +func (c *BitwardenClient) AccessTokenLogin(accessToken string) error { + req := AccessTokenLoginRequest{AccessToken: accessToken} + command := Command{AccessTokenLogin: &req} + + responseStr, err := c.commandRunner.RunCommand(command) + if err != nil { + return err + } + + var response APIKeyLoginResponse + return checkSuccessAndError(responseStr, &response) +} + +func (c *BitwardenClient) Close() { + c.lib.FreeMem(c.client) +} diff --git a/languages/go/command_runner.go b/languages/go/command_runner.go new file mode 100644 index 000000000..3f79f7149 --- /dev/null +++ b/languages/go/command_runner.go @@ -0,0 +1,37 @@ +package sdk + +import ( + "encoding/json" + + "github.com/bitwarden/sdk/languages/go/internal/cinterface" +) + +type CommandRunnerInterface interface { + RunCommand(command Command) (string, error) +} + +type CommandRunner struct { + client cinterface.ClientPointer + lib cinterface.BitwardenLibrary +} + +func NewCommandRunner(client cinterface.ClientPointer, lib cinterface.BitwardenLibrary) *CommandRunner { + return &CommandRunner{ + client: client, + lib: lib, + } +} + +func (c *CommandRunner) RunCommand(command Command) (string, error) { + commandJSON, err := json.Marshal(command) + if err != nil { + return "", err + } + + responseStr, err := c.lib.RunCommand(string(commandJSON), c.client) + if err != nil { + return "", err + } + + return responseStr, nil +} diff --git a/languages/go/example/example.go b/languages/go/example/example.go new file mode 100644 index 000000000..5935d8002 --- /dev/null +++ b/languages/go/example/example.go @@ -0,0 +1,86 @@ +package main + +import ( + "fmt" + "os" + + sdk "github.com/bitwarden/sdk/languages/go" + "github.com/gofrs/uuid" +) + +func main() { + apiURL := os.Getenv("API_URL") + identityURL := os.Getenv("IDENTITY_URL") + + bitwardenClient, _ := sdk.NewBitwardenClient(&apiURL, &identityURL) + + accessToken := os.Getenv("ACCESS_TOKEN") + organizationIDStr := os.Getenv("ORGANIZATION_ID") + projectName := os.Getenv("PROJECT_NAME") + + if projectName == "" { + projectName = "NewTestProject" // default value + } + + err := bitwardenClient.AccessTokenLogin(accessToken) + if err != nil { + panic(err) + } + + organizationID, err := uuid.FromString(organizationIDStr) + if err != nil { + panic(err) + } + + project, err := bitwardenClient.Projects.Create(organizationID.String(), projectName) + if err != nil { + panic(err) + } + fmt.Println(project) + projectID := project.ID + fmt.Println(projectID) + + if _, err = bitwardenClient.Projects.List(organizationID.String()); err != nil { + panic(err) + } + + if _, err = bitwardenClient.Projects.Get(projectID); err != nil { + panic(err) + } + + if _, err = bitwardenClient.Projects.Update(projectID, organizationID.String(), projectName+"2"); err != nil { + panic(err) + } + + key := "key" + value := "value" + note := "note" + + secret, err := bitwardenClient.Secrets.Create(key, value, note, organizationID.String(), []string{projectID}) + if err != nil { + panic(err) + } + secretID := secret.ID + + if _, err = bitwardenClient.Secrets.List(organizationID.String()); err != nil { + panic(err) + } + + if _, err = bitwardenClient.Secrets.Get(secretID); err != nil { + panic(err) + } + + if _, err = bitwardenClient.Secrets.Update(secretID, key, value, note, organizationID.String(), []string{projectID}); err != nil { + panic(err) + } + + if _, err = bitwardenClient.Secrets.Delete([]string{secretID}); err != nil { + panic(err) + } + + if _, err = bitwardenClient.Projects.Delete([]string{projectID}); err != nil { + panic(err) + } + + defer bitwardenClient.Close() +} diff --git a/languages/go/example/go.mod b/languages/go/example/go.mod new file mode 100644 index 000000000..bbde28fd5 --- /dev/null +++ b/languages/go/example/go.mod @@ -0,0 +1,10 @@ +module example + +replace github.com/bitwarden/sdk/languages/go => ../ + +go 1.20 + +require ( + github.com/bitwarden/sdk/languages/go v0.0.0-00010101000000-000000000000 + github.com/gofrs/uuid v4.4.0+incompatible +) diff --git a/languages/go/example/go.sum b/languages/go/example/go.sum new file mode 100644 index 000000000..c0ad68738 --- /dev/null +++ b/languages/go/example/go.sum @@ -0,0 +1,2 @@ +github.com/gofrs/uuid v4.4.0+incompatible h1:3qXRTX8/NbyulANqlc0lchS1gqAVxRgsuW1YrTJupqA= +github.com/gofrs/uuid v4.4.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM= diff --git a/languages/go/go.mod b/languages/go/go.mod new file mode 100644 index 000000000..a9e125453 --- /dev/null +++ b/languages/go/go.mod @@ -0,0 +1,5 @@ +module github.com/bitwarden/sdk/languages/go + +go 1.18 + +require github.com/gofrs/uuid v4.4.0+incompatible diff --git a/languages/go/go.sum b/languages/go/go.sum new file mode 100644 index 000000000..c0ad68738 --- /dev/null +++ b/languages/go/go.sum @@ -0,0 +1,2 @@ +github.com/gofrs/uuid v4.4.0+incompatible h1:3qXRTX8/NbyulANqlc0lchS1gqAVxRgsuW1YrTJupqA= +github.com/gofrs/uuid v4.4.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM= diff --git a/languages/go/internal/cinterface/bitwarden_library.go b/languages/go/internal/cinterface/bitwarden_library.go new file mode 100644 index 000000000..42327adb2 --- /dev/null +++ b/languages/go/internal/cinterface/bitwarden_library.go @@ -0,0 +1,55 @@ +package cinterface + +import ( + "fmt" + "unsafe" +) + +/* +#cgo LDFLAGS: -lbitwarden_c +#cgo linux LDFLAGS: -L/usr/local/lib -L/usr/lib -L ./lib +#cgo darwin LDFLAGS: -L/usr/local/lib -L/usr/lib -L ./lib +#include +typedef void* ClientPtr; +extern char* run_command(const char *command, ClientPtr client); +extern ClientPtr init(const char *clientSettings); +extern void free_mem(ClientPtr client); +*/ +import "C" + +type ClientPointer struct { + Pointer C.ClientPtr +} + +type BitwardenLibrary interface { + Init(clientSettings string) (ClientPointer, error) + FreeMem(client ClientPointer) + RunCommand(command string, client ClientPointer) (string, error) +} + +type BitwardenLibraryImpl struct{} + +func NewBitwardenLibrary() BitwardenLibrary { + return &BitwardenLibraryImpl{} +} + +func (b *BitwardenLibraryImpl) Init(clientSettings string) (ClientPointer, error) { + ptr := C.init(C.CString(clientSettings)) + if ptr == nil { + return ClientPointer{}, fmt.Errorf("initialization failed") + } + return ClientPointer{Pointer: ptr}, nil +} + +func (b *BitwardenLibraryImpl) FreeMem(client ClientPointer) { + C.free_mem(client.Pointer) +} + +func (b *BitwardenLibraryImpl) RunCommand(command string, client ClientPointer) (string, error) { + cstr := C.run_command(C.CString(command), client.Pointer) + if cstr == nil { + return "", fmt.Errorf("run command failed") + } + defer C.free(unsafe.Pointer(cstr)) + return C.GoString(cstr), nil +} diff --git a/languages/go/project.go b/languages/go/project.go new file mode 100644 index 000000000..24a30a7ac --- /dev/null +++ b/languages/go/project.go @@ -0,0 +1,107 @@ +package sdk + +type ProjectsInterface interface { + Create(organizationID string, name string) (*ProjectResponse, error) + List(organizationID string) (*ProjectsResponse, error) + Get(projectID string) (*ProjectResponse, error) + Update(projectID string, organizationID string, name string) (*ProjectResponse, error) + Delete(projectIDs []string) (*ProjectsDeleteResponse, error) +} + +type Projects struct { + CommandRunner CommandRunnerInterface +} + +func NewProjects(commandRunner CommandRunnerInterface) *Projects { + return &Projects{CommandRunner: commandRunner} +} + +func (p *Projects) Get(id string) (*ProjectResponse, error) { + command := Command{ + Projects: &ProjectsCommand{ + Get: &ProjectGetRequest{ + ID: id, + }, + }, + } + var response ProjectResponse + if err := p.executeCommand(command, &response); err != nil { + return nil, err + } + return &response, nil +} + +func (p *Projects) Create(organizationID string, name string) (*ProjectResponse, error) { + command := Command{ + Projects: &ProjectsCommand{ + Create: &ProjectCreateRequest{ + OrganizationID: organizationID, + Name: name, + }, + }, + } + + var response ProjectResponse + if err := p.executeCommand(command, &response); err != nil { + return nil, err + } + return &response, nil +} + +func (p *Projects) List(organizationID string) (*ProjectsResponse, error) { + command := Command{ + Projects: &ProjectsCommand{ + List: &ProjectsListRequest{ + OrganizationID: organizationID, + }, + }, + } + + var response ProjectsResponse + if err := p.executeCommand(command, &response); err != nil { + return nil, err + } + return &response, nil +} + +func (p *Projects) Update(projectID, organizationID, name string) (*ProjectResponse, error) { + command := Command{ + Projects: &ProjectsCommand{ + Update: &ProjectPutRequest{ + ID: projectID, + OrganizationID: organizationID, + Name: name, + }, + }, + } + + var response ProjectResponse + if err := p.executeCommand(command, &response); err != nil { + return nil, err + } + return &response, nil +} + +func (p *Projects) Delete(projectIDs []string) (*ProjectsDeleteResponse, error) { + command := Command{ + Projects: &ProjectsCommand{ + Delete: &ProjectsDeleteRequest{ + IDS: projectIDs, + }, + }, + } + + var response ProjectsDeleteResponse + if err := p.executeCommand(command, &response); err != nil { + return nil, err + } + return &response, nil +} + +func (p *Projects) executeCommand(command Command, target interface{}) error { + responseStr, err := p.CommandRunner.RunCommand(command) + if err != nil { + return err + } + return checkSuccessAndError(responseStr, target) +} diff --git a/languages/go/secrets.go b/languages/go/secrets.go new file mode 100644 index 000000000..e6863c459 --- /dev/null +++ b/languages/go/secrets.go @@ -0,0 +1,114 @@ +package sdk + +type SecretsInterface interface { + Create(key, value, note string, organizationID string, projectIDs []string) (*SecretResponse, error) + List(organizationID string) (*SecretIdentifiersResponse, error) + Get(secretID string) (*SecretResponse, error) + Update(secretID string, key, value, note string, organizationID string, projectIDs []string) (*SecretResponse, error) + Delete(secretIDs []string) (*SecretsDeleteResponse, error) +} + +type Secrets struct { + CommandRunner CommandRunnerInterface +} + +func NewSecrets(commandRunner CommandRunnerInterface) *Secrets { + return &Secrets{CommandRunner: commandRunner} +} + +func (s *Secrets) executeCommand(command Command, target interface{}) error { + responseStr, err := s.CommandRunner.RunCommand(command) + if err != nil { + return err + } + return checkSuccessAndError(responseStr, target) +} + +func (s *Secrets) Create(key, value, note string, organizationID string, projectIDs []string) (*SecretResponse, error) { + command := Command{ + Secrets: &SecretsCommand{ + Create: &SecretCreateRequest{ + Key: key, + Value: value, + Note: note, + OrganizationID: organizationID, + ProjectIDS: projectIDs, + }, + }, + } + + var response SecretResponse + if err := s.executeCommand(command, &response); err != nil { + return nil, err + } + return &response, nil +} + +func (s *Secrets) List(organizationID string) (*SecretIdentifiersResponse, error) { + command := Command{ + Secrets: &SecretsCommand{ + List: &SecretIdentifiersRequest{ + OrganizationID: organizationID, + }, + }, + } + + var response SecretIdentifiersResponse + if err := s.executeCommand(command, &response); err != nil { + return nil, err + } + return &response, nil +} + +func (s *Secrets) Get(id string) (*SecretResponse, error) { + command := Command{ + Secrets: &SecretsCommand{ + Get: &SecretGetRequest{ + ID: id, + }, + }, + } + + var response SecretResponse + if err := s.executeCommand(command, &response); err != nil { + return nil, err + } + return &response, nil +} + +func (s *Secrets) Update(id string, key, value, note string, organizationID string, projectIDs []string) (*SecretResponse, error) { + command := Command{ + Secrets: &SecretsCommand{ + Update: &SecretPutRequest{ + ID: id, + Key: key, + Value: value, + Note: note, + OrganizationID: organizationID, + ProjectIDS: projectIDs, + }, + }, + } + + var response SecretResponse + if err := s.executeCommand(command, &response); err != nil { + return nil, err + } + return &response, nil +} + +func (s *Secrets) Delete(ids []string) (*SecretsDeleteResponse, error) { + command := Command{ + Secrets: &SecretsCommand{ + Delete: &SecretsDeleteRequest{ + IDS: ids, + }, + }, + } + + var response SecretsDeleteResponse + if err := s.executeCommand(command, &response); err != nil { + return nil, err + } + return &response, nil +} diff --git a/languages/go/util.go b/languages/go/util.go new file mode 100644 index 000000000..01ab3578d --- /dev/null +++ b/languages/go/util.go @@ -0,0 +1,33 @@ +package sdk + +import ( + "encoding/json" + "fmt" +) + +func checkSuccessAndError(responseStr string, v interface{}) error { + var wrapper struct { + Success bool `json:"success"` + ErrorMessage *string `json:"errorMessage"` + Data *json.RawMessage `json:"data"` + } + + err := json.Unmarshal([]byte(responseStr), &wrapper) + if err != nil { + return fmt.Errorf("failed to unmarshal wrapper response: %v", err) + } + + if !wrapper.Success { + if wrapper.ErrorMessage != nil { + return fmt.Errorf("API error: %s", *wrapper.ErrorMessage) + } + return fmt.Errorf("API error: unknown") + } + + err = json.Unmarshal(*wrapper.Data, &v) + if err != nil { + return fmt.Errorf("failed to unmarshal response: %v", err) + } + + return nil +} diff --git a/languages/java/.gitignore b/languages/java/.gitignore new file mode 100644 index 000000000..b5b6c5697 --- /dev/null +++ b/languages/java/.gitignore @@ -0,0 +1,3 @@ +target +.gradle +src/main/resources diff --git a/languages/java/README.md b/languages/java/README.md new file mode 100644 index 000000000..06f1ffec3 --- /dev/null +++ b/languages/java/README.md @@ -0,0 +1,73 @@ +# Bitwarden Secrets Manager SDK + +Java bindings for interacting with the [Bitwarden Secrets Manager]. This is a beta release and might be missing some +functionality. + +## Create access token + +Review the help documentation on [Access Tokens] + +## Usage code snippets + +### Create new Bitwarden client + +```java +BitwardenSettings bitwardenSettings = new BitwardenSettings(); +bitwardenSettings.setApiUrl("https://api.bitwarden.com"); +bitwardenSettings.setIdentityUrl("https://identity.bitwarden.com"); +BitwardenClient bitwardenClient = new BitwardenClient(bitwardenSettings); +bitwardenClient.accessTokenLogin(""); +``` + +### Create new project + +```java +UUID organizationId = UUID.fromString(""); +var projectResponse = bitwardenClient.projects().create(organizationId, "TestProject"); +``` + +### List all projects + +```java +var projectsResponse = bitwardenClient.projects().list(organizationId); +``` + +### Update project + +```java +UUID projectId = projectResponse.getID(); +projectResponse = bitwardenClient.projects().get(projectId); +projectResponse = bitwardenClient.projects.update(projectId, organizationId, "TestProjectUpdated"); +``` + +### Add new secret + +```java +String key = "key"; +String value = "value"; +String note = "note"; +var secretResponse = bitwardenClient.secrets().create(key, value, note, organizationId, new UUID[]{projectId}); +UUID secretId = secretResponse.getID(); +``` + +### Update secret + +```java +bitwardenClient.secrets().update(secretId, key2, value2, note2, organizationId, new UUID[]{projectId}); +``` + +### List secrets + +```java +var secretIdentifiersResponse secretIdentifiersResponse = bitwardenClient.secrets().list(organizationId); +``` + +# Delete secret or project + +```java +bitwardenClient.secrets().delete(new UUID[]{secretId}); +bitwardenClient.projects().delete(new UUID[]{projectId}); +``` + +[Access Tokens]: https://bitwarden.com/help/access-tokens/ +[Bitwarden Secrets Manager]: https://bitwarden.com/products/secrets-manager/ \ No newline at end of file diff --git a/languages/java/build.gradle b/languages/java/build.gradle new file mode 100644 index 000000000..dc17386c4 --- /dev/null +++ b/languages/java/build.gradle @@ -0,0 +1,90 @@ +/* + * This file was generated by the Gradle 'init' task. + */ + +plugins { + id 'java-library' + id 'maven-publish' +} + +repositories { + mavenLocal() + maven { + url = uri('https://repo.maven.apache.org/maven2/') + } + + dependencies { + api 'com.fasterxml.jackson.core:jackson-core:2.9.10' + api 'com.fasterxml.jackson.datatype:jackson-datatype-jsr310:2.9.10' + api 'net.java.dev.jna:jna-platform:5.12.1' + } + + description = 'BitwardenSDK' + java.sourceCompatibility = JavaVersion.VERSION_1_8 + + publishing { + publications { + maven(MavenPublication) { + groupId = 'com.bitwarden' + artifactId = 'sdk' + + // Determine the version from the git history. + // + // PRs: use the branch name. + // Master: Grab it from `crates/bitwarden/Cargo.toml` + + def branchName = "git branch --show-current".execute().text.trim() + + if (branchName == "master") { + def content = ['grep', '-o', '^version = ".*"', '../../crates/bitwarden/Cargo.toml'].execute().text.trim() + def match = ~/version = "(.*)"/ + def matcher = match.matcher(content) + matcher.find() + + version = "${matcher.group(1)}-SNAPSHOT" + } else { + // branchName-SNAPSHOT + version = "${branchName.replaceAll('/', '-')}-SNAPSHOT" + } + + afterEvaluate { + from components.java + } + } + } + repositories { + maven { + name = "GitHubPackages" + url = "https://maven.pkg.github.com/bitwarden/sdk" + credentials { + username = System.getenv("GITHUB_ACTOR") + password = System.getenv("GITHUB_TOKEN") + } + } + } + } +} + +tasks.withType(JavaCompile) { + options.encoding = 'UTF-8' +} + +tasks.withType(Javadoc) { + options.encoding = 'UTF-8' +} + +// Gradle build requires GitHub workflow to copy native library to resources +// Uncomment copyNativeLib and jar tasks to use the local build (modify architecture if needed) +//tasks.register('copyNativeLib', Copy) { +// delete 'src/main/resources/darwin-aarch64' +// from '../../target/debug' +// include '*libbitwarden_c*.dylib' +// include '*libbitwarden_c*.so' +// include '*bitwarden_c*.dll' +// into 'src/main/resources/darwin-aarch64' +//} +// +//jar { +// dependsOn tasks.named("copyNativeLib").get() +// from 'src/main/resources' +//} diff --git a/languages/java/gradle/wrapper/gradle-wrapper.jar b/languages/java/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 000000000..7f93135c4 Binary files /dev/null and b/languages/java/gradle/wrapper/gradle-wrapper.jar differ diff --git a/languages/java/gradle/wrapper/gradle-wrapper.properties b/languages/java/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 000000000..ac72c34e8 --- /dev/null +++ b/languages/java/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,7 @@ +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-8.3-bin.zip +networkTimeout=10000 +validateDistributionUrl=true +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists diff --git a/languages/java/gradlew b/languages/java/gradlew new file mode 100755 index 000000000..0adc8e1a5 --- /dev/null +++ b/languages/java/gradlew @@ -0,0 +1,249 @@ +#!/bin/sh + +# +# Copyright © 2015-2021 the original authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +############################################################################## +# +# Gradle start up script for POSIX generated by Gradle. +# +# Important for running: +# +# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is +# noncompliant, but you have some other compliant shell such as ksh or +# bash, then to run this script, type that shell name before the whole +# command line, like: +# +# ksh Gradle +# +# Busybox and similar reduced shells will NOT work, because this script +# requires all of these POSIX shell features: +# * functions; +# * expansions «$var», «${var}», «${var:-default}», «${var+SET}», +# «${var#prefix}», «${var%suffix}», and «$( cmd )»; +# * compound commands having a testable exit status, especially «case»; +# * various built-in commands including «command», «set», and «ulimit». +# +# Important for patching: +# +# (2) This script targets any POSIX shell, so it avoids extensions provided +# by Bash, Ksh, etc; in particular arrays are avoided. +# +# The "traditional" practice of packing multiple parameters into a +# space-separated string is a well documented source of bugs and security +# problems, so this is (mostly) avoided, by progressively accumulating +# options in "$@", and eventually passing that to Java. +# +# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, +# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; +# see the in-line comments for details. +# +# There are tweaks for specific operating systems such as AIX, CygWin, +# Darwin, MinGW, and NonStop. +# +# (3) This script is generated from the Groovy template +# https://github.com/gradle/gradle/blob/HEAD/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt +# within the Gradle project. +# +# You can find Gradle at https://github.com/gradle/gradle/. +# +############################################################################## + +# Attempt to set APP_HOME + +# Resolve links: $0 may be a link +app_path=$0 + +# Need this for daisy-chained symlinks. +while + APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path + [ -h "$app_path" ] +do + ls=$( ls -ld "$app_path" ) + link=${ls#*' -> '} + case $link in #( + /*) app_path=$link ;; #( + *) app_path=$APP_HOME$link ;; + esac +done + +# This is normally unused +# shellcheck disable=SC2034 +APP_BASE_NAME=${0##*/} +# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036) +APP_HOME=$( cd "${APP_HOME:-./}" > /dev/null && pwd -P ) || exit + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD=maximum + +warn () { + echo "$*" +} >&2 + +die () { + echo + echo "$*" + echo + exit 1 +} >&2 + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +nonstop=false +case "$( uname )" in #( + CYGWIN* ) cygwin=true ;; #( + Darwin* ) darwin=true ;; #( + MSYS* | MINGW* ) msys=true ;; #( + NONSTOP* ) nonstop=true ;; +esac + +CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD=$JAVA_HOME/jre/sh/java + else + JAVACMD=$JAVA_HOME/bin/java + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD=java + if ! command -v java >/dev/null 2>&1 + then + die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +fi + +# Increase the maximum file descriptors if we can. +if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then + case $MAX_FD in #( + max*) + # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC3045 + MAX_FD=$( ulimit -H -n ) || + warn "Could not query maximum file descriptor limit" + esac + case $MAX_FD in #( + '' | soft) :;; #( + *) + # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC3045 + ulimit -n "$MAX_FD" || + warn "Could not set maximum file descriptor limit to $MAX_FD" + esac +fi + +# Collect all arguments for the java command, stacking in reverse order: +# * args from the command line +# * the main class name +# * -classpath +# * -D...appname settings +# * --module-path (only if needed) +# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. + +# For Cygwin or MSYS, switch paths to Windows format before running java +if "$cygwin" || "$msys" ; then + APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) + CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) + + JAVACMD=$( cygpath --unix "$JAVACMD" ) + + # Now convert the arguments - kludge to limit ourselves to /bin/sh + for arg do + if + case $arg in #( + -*) false ;; # don't mess with options #( + /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath + [ -e "$t" ] ;; #( + *) false ;; + esac + then + arg=$( cygpath --path --ignore --mixed "$arg" ) + fi + # Roll the args list around exactly as many times as the number of + # args, so each arg winds up back in the position where it started, but + # possibly modified. + # + # NB: a `for` loop captures its iteration list before it begins, so + # changing the positional parameters here affects neither the number of + # iterations, nor the values presented in `arg`. + shift # remove old arg + set -- "$@" "$arg" # push replacement arg + done +fi + + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' + +# Collect all arguments for the java command; +# * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of +# shell script including quotes and variable substitutions, so put them in +# double quotes to make sure that they get re-expanded; and +# * put everything else in single quotes, so that it's not re-expanded. + +set -- \ + "-Dorg.gradle.appname=$APP_BASE_NAME" \ + -classpath "$CLASSPATH" \ + org.gradle.wrapper.GradleWrapperMain \ + "$@" + +# Stop when "xargs" is not available. +if ! command -v xargs >/dev/null 2>&1 +then + die "xargs is not available" +fi + +# Use "xargs" to parse quoted args. +# +# With -n1 it outputs one arg per line, with the quotes and backslashes removed. +# +# In Bash we could simply go: +# +# readarray ARGS < <( xargs -n1 <<<"$var" ) && +# set -- "${ARGS[@]}" "$@" +# +# but POSIX shell has neither arrays nor command substitution, so instead we +# post-process each arg (as a line of input to sed) to backslash-escape any +# character that might be a shell metacharacter, then use eval to reverse +# that process (while maintaining the separation between arguments), and wrap +# the whole thing up as a single "set" statement. +# +# This will of course break if any of these variables contains a newline or +# an unmatched quote. +# + +eval "set -- $( + printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | + xargs -n1 | + sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | + tr '\n' ' ' + )" '"$@"' + +exec "$JAVACMD" "$@" diff --git a/languages/java/gradlew.bat b/languages/java/gradlew.bat new file mode 100644 index 000000000..6689b85be --- /dev/null +++ b/languages/java/gradlew.bat @@ -0,0 +1,92 @@ +@rem +@rem Copyright 2015 the original author or authors. +@rem +@rem Licensed under the Apache License, Version 2.0 (the "License"); +@rem you may not use this file except in compliance with the License. +@rem You may obtain a copy of the License at +@rem +@rem https://www.apache.org/licenses/LICENSE-2.0 +@rem +@rem Unless required by applicable law or agreed to in writing, software +@rem distributed under the License is distributed on an "AS IS" BASIS, +@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +@rem See the License for the specific language governing permissions and +@rem limitations under the License. +@rem + +@if "%DEBUG%"=="" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +set DIRNAME=%~dp0 +if "%DIRNAME%"=="" set DIRNAME=. +@rem This is normally unused +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Resolve any "." and ".." in APP_HOME to make it shorter. +for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if %ERRORLEVEL% equ 0 goto execute + +echo. +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto execute + +echo. +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:execute +@rem Setup the command line + +set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* + +:end +@rem End local scope for the variables with windows NT shell +if %ERRORLEVEL% equ 0 goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +set EXIT_CODE=%ERRORLEVEL% +if %EXIT_CODE% equ 0 set EXIT_CODE=1 +if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% +exit /b %EXIT_CODE% + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/languages/java/settings.gradle b/languages/java/settings.gradle new file mode 100644 index 000000000..961795f50 --- /dev/null +++ b/languages/java/settings.gradle @@ -0,0 +1,5 @@ +/* + * This file was generated by the Gradle 'init' task. + */ + +rootProject.name = 'BitwardenSDK' diff --git a/languages/java/src/main/java/com/bitwarden/sdk/BitwardenClient.java b/languages/java/src/main/java/com/bitwarden/sdk/BitwardenClient.java new file mode 100644 index 000000000..9e1948184 --- /dev/null +++ b/languages/java/src/main/java/com/bitwarden/sdk/BitwardenClient.java @@ -0,0 +1,87 @@ +package com.bitwarden.sdk; + +import com.bitwarden.sdk.schema.*; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.sun.jna.Native; +import com.sun.jna.Pointer; + +import java.util.function.Function; + +public class BitwardenClient implements AutoCloseable { + + private final Pointer client; + + private final BitwardenLibrary library; + + private final CommandRunner commandRunner; + + private boolean isClientOpen; + + private final ProjectsClient projects; + + private final SecretsClient secrets; + + public BitwardenClient(BitwardenSettings bitwardenSettings) { + ClientSettings clientSettings = new ClientSettings(); + clientSettings.setAPIURL(bitwardenSettings.getApiUrl()); + clientSettings.setIdentityURL(bitwardenSettings.getIdentityUrl()); + clientSettings.setDeviceType(DeviceType.SDK); + clientSettings.setUserAgent("Bitwarden JAVA-SDK"); + + library = Native.load("bitwarden_c", BitwardenLibrary.class); + + try { + client = library.init(Converter.ClientSettingsToJsonString(clientSettings)); + } catch (JsonProcessingException e) { + throw new BitwardenClientException("Error while processing client settings"); + } + + commandRunner = new CommandRunner(library, client); + projects = new ProjectsClient(commandRunner); + secrets = new SecretsClient(commandRunner); + isClientOpen = true; + } + + static Function throwingFunctionWrapper(ThrowingFunction throwingFunction) { + + return i -> { + try { + return throwingFunction.accept(i); + } catch (Exception ex) { + throw new BitwardenClientException("Response deserialization failed"); + } + }; + } + + public APIKeyLoginResponse accessTokenLogin(String accessToken) { + Command command = new Command(); + AccessTokenLoginRequest accessTokenLoginRequest = new AccessTokenLoginRequest(); + accessTokenLoginRequest.setAccessToken(accessToken); + command.setAccessTokenLogin(accessTokenLoginRequest); + + ResponseForAPIKeyLoginResponse response = commandRunner.runCommand(command, + throwingFunctionWrapper(Converter::ResponseForAPIKeyLoginResponseFromJsonString)); + + if (response == null || !response.getSuccess()) { + throw new BitwardenClientException(response != null ? response.getErrorMessage() : "Login failed"); + } + + return response.getData(); + } + + public ProjectsClient projects() { + return projects; + } + + public SecretsClient secrets() { + return secrets; + } + + @Override + public void close() { + if (isClientOpen) { + library.free_mem(client); + isClientOpen = false; + } + } +} diff --git a/languages/java/src/main/java/com/bitwarden/sdk/BitwardenClientException.java b/languages/java/src/main/java/com/bitwarden/sdk/BitwardenClientException.java new file mode 100644 index 000000000..7b6025be8 --- /dev/null +++ b/languages/java/src/main/java/com/bitwarden/sdk/BitwardenClientException.java @@ -0,0 +1,8 @@ +package com.bitwarden.sdk; + +public class BitwardenClientException extends RuntimeException { + + public BitwardenClientException(String message) { + super(message); + } +} diff --git a/languages/java/src/main/java/com/bitwarden/sdk/BitwardenLibrary.java b/languages/java/src/main/java/com/bitwarden/sdk/BitwardenLibrary.java new file mode 100644 index 000000000..73d81452c --- /dev/null +++ b/languages/java/src/main/java/com/bitwarden/sdk/BitwardenLibrary.java @@ -0,0 +1,13 @@ +package com.bitwarden.sdk; + +import com.sun.jna.Library; +import com.sun.jna.Pointer; + +public interface BitwardenLibrary extends Library { + + Pointer init(String clientSettings); + + void free_mem(Pointer client); + + String run_command(String command, Pointer client); +} diff --git a/languages/java/src/main/java/com/bitwarden/sdk/BitwardenSettings.java b/languages/java/src/main/java/com/bitwarden/sdk/BitwardenSettings.java new file mode 100644 index 000000000..7112297b4 --- /dev/null +++ b/languages/java/src/main/java/com/bitwarden/sdk/BitwardenSettings.java @@ -0,0 +1,32 @@ +package com.bitwarden.sdk; + +public class BitwardenSettings { + + private String apiUrl; + + private String identityUrl; + + public BitwardenSettings() { + } + + public BitwardenSettings(String apiUrl, String identityUrl) { + this.apiUrl = apiUrl; + this.identityUrl = identityUrl; + } + + public String getApiUrl() { + return apiUrl; + } + + public void setApiUrl(String apiUrl) { + this.apiUrl = apiUrl; + } + + public String getIdentityUrl() { + return identityUrl; + } + + public void setIdentityUrl(String identityUrl) { + this.identityUrl = identityUrl; + } +} diff --git a/languages/java/src/main/java/com/bitwarden/sdk/CommandRunner.java b/languages/java/src/main/java/com/bitwarden/sdk/CommandRunner.java new file mode 100644 index 000000000..11a100814 --- /dev/null +++ b/languages/java/src/main/java/com/bitwarden/sdk/CommandRunner.java @@ -0,0 +1,45 @@ +package com.bitwarden.sdk; + +import com.bitwarden.sdk.schema.Command; +import com.bitwarden.sdk.schema.Converter; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.sun.jna.Pointer; + +import java.io.IOException; +import java.util.function.Function; + +class CommandRunner { + + private final BitwardenLibrary library; + + private final Pointer client; + + CommandRunner(BitwardenLibrary library, Pointer client) { + this.library = library; + this.client = client; + } + + T runCommand(Command command, Function deserializer) { + String response = null; + + try { + response = library.run_command(commandToString(command), client); + } catch (IOException e) { + throw new RuntimeException(e); + } + + return deserializer.apply(response); + } + + private String commandToString(Command command) throws IOException { + // Removes null properties from the generated converter output to avoid command errors + String inputJson = Converter.CommandToJsonString(command); + + ObjectMapper mapper = new ObjectMapper(); + mapper.setSerializationInclusion(JsonInclude.Include.NON_NULL); + + Object inputObject = mapper.readValue(inputJson, Object.class); + return mapper.writeValueAsString(inputObject); + } +} diff --git a/languages/java/src/main/java/com/bitwarden/sdk/ExampleProgram.java b/languages/java/src/main/java/com/bitwarden/sdk/ExampleProgram.java new file mode 100644 index 000000000..f3ad53036 --- /dev/null +++ b/languages/java/src/main/java/com/bitwarden/sdk/ExampleProgram.java @@ -0,0 +1,42 @@ +package com.bitwarden.sdk; + +import com.bitwarden.sdk.schema.*; + +import java.util.UUID; + +public class ExampleProgram { + + public static void main(String[] args) { + BitwardenSettings bitwardenSettings = new BitwardenSettings(); + bitwardenSettings.setApiUrl("https://api.bitwarden.com"); + bitwardenSettings.setIdentityUrl("https://identity.bitwarden.com"); + + try (BitwardenClient bitwardenClient = new BitwardenClient(bitwardenSettings)) { + APIKeyLoginResponse apiKeyLoginResponse = bitwardenClient.accessTokenLogin(""); + + UUID organizationId = UUID.fromString(""); + ProjectResponse projectResponse = bitwardenClient.projects().create(organizationId, "NewTestProject"); + UUID projectId = projectResponse.getID(); + ProjectsResponse projectsResponse = bitwardenClient.projects().list(organizationId); + projectResponse = bitwardenClient.projects().get(projectId); + projectResponse = bitwardenClient.projects().update(projectId, organizationId, "NewTestProject2"); + + String key = "key"; + String value = "value"; + String note = "note"; + SecretResponse secretResponse = bitwardenClient.secrets().create(key, value, note, organizationId, + new UUID[]{projectId}); + UUID secretId = secretResponse.getID(); + SecretIdentifiersResponse secretIdentifiersResponse = bitwardenClient.secrets().list(organizationId); + secretResponse = bitwardenClient.secrets().get(secretId); + key = "key2"; + value = "value2"; + note = "note2"; + secretResponse = bitwardenClient.secrets().update(secretId, key, value, note, organizationId, + new UUID[]{projectId}); + + SecretsDeleteResponse secretsDeleteResponse = bitwardenClient.secrets().delete(new UUID[]{secretId}); + ProjectsDeleteResponse projectsDeleteResponse = bitwardenClient.projects().delete(new UUID[]{projectId}); + } + } +} diff --git a/languages/java/src/main/java/com/bitwarden/sdk/ProjectsClient.java b/languages/java/src/main/java/com/bitwarden/sdk/ProjectsClient.java new file mode 100644 index 000000000..73b87440c --- /dev/null +++ b/languages/java/src/main/java/com/bitwarden/sdk/ProjectsClient.java @@ -0,0 +1,109 @@ +package com.bitwarden.sdk; + +import com.bitwarden.sdk.schema.*; + +import java.util.UUID; + +public class ProjectsClient { + + private final CommandRunner commandRunner; + + ProjectsClient(CommandRunner commandRunner) { + this.commandRunner = commandRunner; + } + + public ProjectResponse get(UUID id) { + Command command = new Command(); + ProjectsCommand projectsCommand = new ProjectsCommand(); + ProjectGetRequest projectGetRequest = new ProjectGetRequest(); + projectGetRequest.setID(id); + projectsCommand.setGet(projectGetRequest); + command.setProjects(projectsCommand); + + ResponseForProjectResponse response = commandRunner.runCommand(command, + BitwardenClient.throwingFunctionWrapper(Converter::ResponseForProjectResponseFromJsonString)); + + if (response == null || !response.getSuccess()) { + throw new BitwardenClientException(response != null ? response.getErrorMessage() : "Project not found"); + } + + return response.getData(); + } + + public ProjectResponse create(UUID organizationId, String name) { + Command command = new Command(); + ProjectsCommand projectsCommand = new ProjectsCommand(); + ProjectCreateRequest projectCreateRequest = new ProjectCreateRequest(); + projectCreateRequest.setOrganizationID(organizationId); + projectCreateRequest.setName(name); + projectsCommand.setCreate(projectCreateRequest); + command.setProjects(projectsCommand); + + ResponseForProjectResponse response = commandRunner.runCommand(command, + BitwardenClient.throwingFunctionWrapper(Converter::ResponseForProjectResponseFromJsonString)); + + if (response == null || !response.getSuccess()) { + throw new BitwardenClientException(response != null ? response.getErrorMessage() : "Project create failed"); + } + + return response.getData(); + } + + public ProjectResponse update(UUID id, UUID organizationId, String name) { + Command command = new Command(); + ProjectsCommand projectsCommand = new ProjectsCommand(); + ProjectPutRequest projectPutRequest = new ProjectPutRequest(); + projectPutRequest.setID(id); + projectPutRequest.setOrganizationID(organizationId); + projectPutRequest.setName(name); + projectsCommand.setUpdate(projectPutRequest); + command.setProjects(projectsCommand); + + ResponseForProjectResponse response = commandRunner.runCommand(command, + BitwardenClient.throwingFunctionWrapper(Converter::ResponseForProjectResponseFromJsonString)); + + if (response == null || !response.getSuccess()) { + throw new BitwardenClientException(response != null ? response.getErrorMessage() : "Project update failed"); + } + + return response.getData(); + } + + public ProjectsDeleteResponse delete(UUID[] ids) { + Command command = new Command(); + ProjectsCommand projectsCommand = new ProjectsCommand(); + ProjectsDeleteRequest projectsDeleteRequest = new ProjectsDeleteRequest(); + projectsDeleteRequest.setIDS(ids); + projectsCommand.setDelete(projectsDeleteRequest); + command.setProjects(projectsCommand); + + ResponseForProjectsDeleteResponse response = commandRunner.runCommand(command, + BitwardenClient.throwingFunctionWrapper(Converter::ResponseForProjectsDeleteResponseFromJsonString)); + + if (response == null || !response.getSuccess()) { + throw new BitwardenClientException(response != null ? + response.getErrorMessage() : "Projects update failed"); + } + + return response.getData(); + } + + public ProjectsResponse list(UUID organizationId) { + Command command = new Command(); + ProjectsCommand projectsCommand = new ProjectsCommand(); + ProjectsListRequest projectsListRequest = new ProjectsListRequest(); + projectsListRequest.setOrganizationID(organizationId); + projectsCommand.setList(projectsListRequest); + command.setProjects(projectsCommand); + + ResponseForProjectsResponse response = commandRunner.runCommand(command, + BitwardenClient.throwingFunctionWrapper(Converter::ResponseForProjectsResponseFromJsonString)); + + if (response == null || !response.getSuccess()) { + throw new BitwardenClientException(response != null ? + response.getErrorMessage() : "No projects for given organization"); + } + + return response.getData(); + } +} diff --git a/languages/java/src/main/java/com/bitwarden/sdk/SecretsClient.java b/languages/java/src/main/java/com/bitwarden/sdk/SecretsClient.java new file mode 100644 index 000000000..f1a97fdc7 --- /dev/null +++ b/languages/java/src/main/java/com/bitwarden/sdk/SecretsClient.java @@ -0,0 +1,115 @@ +package com.bitwarden.sdk; + +import com.bitwarden.sdk.schema.*; + +import java.util.UUID; + +public class SecretsClient { + + private final CommandRunner commandRunner; + + SecretsClient(CommandRunner commandRunner) { + this.commandRunner = commandRunner; + } + + public SecretResponse get(UUID id) { + Command command = new Command(); + SecretsCommand secretsCommand = new SecretsCommand(); + SecretGetRequest secretGetRequest = new SecretGetRequest(); + secretGetRequest.setID(id); + secretsCommand.setGet(secretGetRequest); + command.setSecrets(secretsCommand); + + ResponseForSecretResponse response = commandRunner.runCommand(command, + BitwardenClient.throwingFunctionWrapper(Converter::ResponseForSecretResponseFromJsonString)); + + if (response == null || !response.getSuccess()) { + throw new BitwardenClientException(response != null ? response.getErrorMessage() : "Secret not found"); + } + + return response.getData(); + } + + public SecretResponse create(String key, String value, String note, UUID organizationId, UUID[] projectIds) { + Command command = new Command(); + SecretsCommand secretsCommand = new SecretsCommand(); + SecretCreateRequest secretCreateRequest = new SecretCreateRequest(); + secretCreateRequest.setKey(key); + secretCreateRequest.setValue(value); + secretCreateRequest.setNote(note); + secretCreateRequest.setOrganizationID(organizationId); + secretCreateRequest.setProjectIDS(projectIds); + secretsCommand.setCreate(secretCreateRequest); + command.setSecrets(secretsCommand); + + ResponseForSecretResponse response = commandRunner.runCommand(command, + BitwardenClient.throwingFunctionWrapper(Converter::ResponseForSecretResponseFromJsonString)); + + if (response == null || !response.getSuccess()) { + throw new BitwardenClientException(response != null ? response.getErrorMessage() : "Secret create failed"); + } + + return response.getData(); + } + + public SecretResponse update(UUID id, String key, String value, String note, UUID organizationId, + UUID[] projectIds) { + Command command = new Command(); + SecretsCommand secretsCommand = new SecretsCommand(); + SecretPutRequest secretPutRequest = new SecretPutRequest(); + secretPutRequest.setID(id); + secretPutRequest.setKey(key); + secretPutRequest.setValue(value); + secretPutRequest.setNote(note); + secretPutRequest.setOrganizationID(organizationId); + secretPutRequest.setProjectIDS(projectIds); + secretsCommand.setUpdate(secretPutRequest); + command.setSecrets(secretsCommand); + + ResponseForSecretResponse response = commandRunner.runCommand(command, + BitwardenClient.throwingFunctionWrapper(Converter::ResponseForSecretResponseFromJsonString)); + + if (response == null || !response.getSuccess()) { + throw new BitwardenClientException(response != null ? response.getErrorMessage() : "Secret update failed"); + } + + return response.getData(); + } + + public SecretsDeleteResponse delete(UUID[] ids) { + Command command = new Command(); + SecretsCommand secretsCommand = new SecretsCommand(); + SecretsDeleteRequest secretsDeleteRequest = new SecretsDeleteRequest(); + secretsDeleteRequest.setIDS(ids); + secretsCommand.setDelete(secretsDeleteRequest); + command.setSecrets(secretsCommand); + + ResponseForSecretsDeleteResponse response = commandRunner.runCommand(command, + BitwardenClient.throwingFunctionWrapper(Converter::ResponseForSecretsDeleteResponseFromJsonString)); + + if (response == null || !response.getSuccess()) { + throw new BitwardenClientException(response != null ? response.getErrorMessage() : "Secrets delete failed"); + } + + return response.getData(); + } + + public SecretIdentifiersResponse list(UUID organizationId) { + Command command = new Command(); + SecretsCommand secretsCommand = new SecretsCommand(); + SecretIdentifiersRequest secretIdentifiersRequest = new SecretIdentifiersRequest(); + secretIdentifiersRequest.setOrganizationID(organizationId); + secretsCommand.setList(secretIdentifiersRequest); + command.setSecrets(secretsCommand); + + ResponseForSecretIdentifiersResponse response = commandRunner.runCommand(command, + BitwardenClient.throwingFunctionWrapper(Converter::ResponseForSecretIdentifiersResponseFromJsonString)); + + if (response == null || !response.getSuccess()) { + throw new BitwardenClientException(response != null ? + response.getErrorMessage() : "No secrets for given organization"); + } + + return response.getData(); + } +} diff --git a/languages/java/src/main/java/com/bitwarden/sdk/ThrowingFunction.java b/languages/java/src/main/java/com/bitwarden/sdk/ThrowingFunction.java new file mode 100644 index 000000000..cab5b1041 --- /dev/null +++ b/languages/java/src/main/java/com/bitwarden/sdk/ThrowingFunction.java @@ -0,0 +1,7 @@ +package com.bitwarden.sdk; + +@FunctionalInterface +public interface ThrowingFunction { + + R accept(T t) throws E; +} diff --git a/languages/js_webassembly/package-lock.json b/languages/js_webassembly/package-lock.json index 066f862be..d5345c00a 100644 --- a/languages/js_webassembly/package-lock.json +++ b/languages/js_webassembly/package-lock.json @@ -7,7 +7,7 @@ "devDependencies": { "html-webpack-plugin": "5.5.3", "text-encoding": "0.7.0", - "ts-loader": "9.5.0", + "ts-loader": "9.5.1", "wasm-pack": "0.12.1", "webpack": "5.89.0", "webpack-cli": "5.1.4", @@ -88,9 +88,9 @@ "dev": true }, "node_modules/@types/body-parser": { - "version": "1.19.4", - "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.4.tgz", - "integrity": "sha512-N7UDG0/xiPQa2D/XrVJXjkWbpqHCd2sBaB32ggRF2l83RhPfamgKGF8gwwqyksS95qUS5ZYF9aF+lLPRlwI2UA==", + "version": "1.19.5", + "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.5.tgz", + "integrity": "sha512-fB3Zu92ucau0iQ0JMCFQE7b/dv8Ot07NI3KaZIkIUNXq82k4eBAqUaneXfleGY9JWskeS9y+u0nXMyspcuQrCg==", "dev": true, "dependencies": { "@types/connect": "*", @@ -98,27 +98,27 @@ } }, "node_modules/@types/bonjour": { - "version": "3.5.12", - "resolved": "https://registry.npmjs.org/@types/bonjour/-/bonjour-3.5.12.tgz", - "integrity": "sha512-ky0kWSqXVxSqgqJvPIkgFkcn4C8MnRog308Ou8xBBIVo39OmUFy+jqNe0nPwLCDFxUpmT9EvT91YzOJgkDRcFg==", + "version": "3.5.13", + "resolved": "https://registry.npmjs.org/@types/bonjour/-/bonjour-3.5.13.tgz", + "integrity": "sha512-z9fJ5Im06zvUL548KvYNecEVlA7cVDkGUi6kZusb04mpyEFKCIZJvloCcmpmLaIahDpOQGHaHmG6imtPMmPXGQ==", "dev": true, "dependencies": { "@types/node": "*" } }, "node_modules/@types/connect": { - "version": "3.4.37", - "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.37.tgz", - "integrity": "sha512-zBUSRqkfZ59OcwXon4HVxhx5oWCJmc0OtBTK05M+p0dYjgN6iTwIL2T/WbsQZrEsdnwaF9cWQ+azOnpPvIqY3Q==", + "version": "3.4.38", + "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.38.tgz", + "integrity": "sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==", "dev": true, "dependencies": { "@types/node": "*" } }, "node_modules/@types/connect-history-api-fallback": { - "version": "1.5.2", - "resolved": "https://registry.npmjs.org/@types/connect-history-api-fallback/-/connect-history-api-fallback-1.5.2.tgz", - "integrity": "sha512-gX2j9x+NzSh4zOhnRPSdPPmTepS4DfxES0AvIFv3jGv5QyeAJf6u6dY5/BAoAJU9Qq1uTvwOku8SSC2GnCRl6Q==", + "version": "1.5.3", + "resolved": "https://registry.npmjs.org/@types/connect-history-api-fallback/-/connect-history-api-fallback-1.5.3.tgz", + "integrity": "sha512-6mfQ6iNvhSKCZJoY6sIG3m0pKkdUcweVNOLuBBKvoWGzl2yRxOJcYOTRyLKt3nxXvBLJWa6QkW//tgbIwJehmA==", "dev": true, "dependencies": { "@types/express-serve-static-core": "*", @@ -126,9 +126,9 @@ } }, "node_modules/@types/eslint": { - "version": "8.44.6", - "resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-8.44.6.tgz", - "integrity": "sha512-P6bY56TVmX8y9J87jHNgQh43h6VVU+6H7oN7hgvivV81K2XY8qJZ5vqPy/HdUoVIelii2kChYVzQanlswPWVFw==", + "version": "8.44.7", + "resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-8.44.7.tgz", + "integrity": "sha512-f5ORu2hcBbKei97U73mf+l9t4zTGl74IqZ0GQk4oVea/VS8tQZYkUveSYojk+frraAVYId0V2WC9O4PTNru2FQ==", "dev": true, "dependencies": { "@types/estree": "*", @@ -136,9 +136,9 @@ } }, "node_modules/@types/eslint-scope": { - "version": "3.7.6", - "resolved": "https://registry.npmjs.org/@types/eslint-scope/-/eslint-scope-3.7.6.tgz", - "integrity": "sha512-zfM4ipmxVKWdxtDaJ3MP3pBurDXOCoyjvlpE3u6Qzrmw4BPbfm4/ambIeTk/r/J0iq/+2/xp0Fmt+gFvXJY2PQ==", + "version": "3.7.7", + "resolved": "https://registry.npmjs.org/@types/eslint-scope/-/eslint-scope-3.7.7.tgz", + "integrity": "sha512-MzMFlSLBqNF2gcHWO0G1vP/YQyfvrxZ0bF+u7mzUdZ1/xK4A4sru+nraZz5i3iEIk1l1uyicaDVTB4QbbEkAYg==", "dev": true, "dependencies": { "@types/eslint": "*", @@ -146,15 +146,15 @@ } }, "node_modules/@types/estree": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.3.tgz", - "integrity": "sha512-CS2rOaoQ/eAgAfcTfq6amKG7bsN+EMcgGY4FAFQdvSj2y1ixvOZTUA9mOtCai7E1SYu283XNw7urKK30nP3wkQ==", + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.5.tgz", + "integrity": "sha512-/kYRxGDLWzHOB7q+wtSUQlFrtcdUccpfy+X+9iMBpHK8QLLhx2wIPYuS5DYtR9Wa/YlZAbIovy7qVdB1Aq6Lyw==", "dev": true }, "node_modules/@types/express": { - "version": "4.17.20", - "resolved": "https://registry.npmjs.org/@types/express/-/express-4.17.20.tgz", - "integrity": "sha512-rOaqlkgEvOW495xErXMsmyX3WKBInbhG5eqojXYi3cGUaLoRDlXa5d52fkfWZT963AZ3v2eZ4MbKE6WpDAGVsw==", + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/@types/express/-/express-4.17.21.tgz", + "integrity": "sha512-ejlPM315qwLpaQlQDTjPdsUFSc6ZsP4AN6AlWnogPjQ7CVi7PYF3YVz+CY3jE2pwYf7E/7HlDAN0rV2GxTG0HQ==", "dev": true, "dependencies": { "@types/body-parser": "*", @@ -164,9 +164,9 @@ } }, "node_modules/@types/express-serve-static-core": { - "version": "4.17.39", - "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-4.17.39.tgz", - "integrity": "sha512-BiEUfAiGCOllomsRAZOiMFP7LAnrifHpt56pc4Z7l9K6ACyN06Ns1JLMBxwkfLOjJRlSf06NwWsT7yzfpaVpyQ==", + "version": "4.17.41", + "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-4.17.41.tgz", + "integrity": "sha512-OaJ7XLaelTgrvlZD8/aa0vvvxZdUmlCn6MtWeB7TkiKW70BQLc9XEPpDLPdbo52ZhXUCrznlWdCHWxJWtdyajA==", "dev": true, "dependencies": { "@types/node": "*", @@ -182,60 +182,60 @@ "dev": true }, "node_modules/@types/http-errors": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/@types/http-errors/-/http-errors-2.0.3.tgz", - "integrity": "sha512-pP0P/9BnCj1OVvQR2lF41EkDG/lWWnDyA203b/4Fmi2eTyORnBtcDoKDwjWQthELrBvWkMOrvSOnZ8OVlW6tXA==", + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/@types/http-errors/-/http-errors-2.0.4.tgz", + "integrity": "sha512-D0CFMMtydbJAegzOyHjtiKPLlvnm3iTZyZRSZoLq2mRhDdmLfIWOCYPfQJ4cu2erKghU++QvjcUjp/5h7hESpA==", "dev": true }, "node_modules/@types/http-proxy": { - "version": "1.17.13", - "resolved": "https://registry.npmjs.org/@types/http-proxy/-/http-proxy-1.17.13.tgz", - "integrity": "sha512-GkhdWcMNiR5QSQRYnJ+/oXzu0+7JJEPC8vkWXK351BkhjraZF+1W13CUYARUvX9+NqIU2n6YHA4iwywsc/M6Sw==", + "version": "1.17.14", + "resolved": "https://registry.npmjs.org/@types/http-proxy/-/http-proxy-1.17.14.tgz", + "integrity": "sha512-SSrD0c1OQzlFX7pGu1eXxSEjemej64aaNPRhhVYUGqXh0BtldAAx37MG8btcumvpgKyZp1F5Gn3JkktdxiFv6w==", "dev": true, "dependencies": { "@types/node": "*" } }, "node_modules/@types/json-schema": { - "version": "7.0.14", - "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.14.tgz", - "integrity": "sha512-U3PUjAudAdJBeC2pgN8uTIKgxrb4nlDF3SF0++EldXQvQBGkpFZMSnwQiIoDU77tv45VgNkl/L4ouD+rEomujw==", + "version": "7.0.15", + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", + "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", "dev": true }, "node_modules/@types/mime": { - "version": "1.3.4", - "resolved": "https://registry.npmjs.org/@types/mime/-/mime-1.3.4.tgz", - "integrity": "sha512-1Gjee59G25MrQGk8bsNvC6fxNiRgUlGn2wlhGf95a59DrprnnHk80FIMMFG9XHMdrfsuA119ht06QPDXA1Z7tw==", + "version": "1.3.5", + "resolved": "https://registry.npmjs.org/@types/mime/-/mime-1.3.5.tgz", + "integrity": "sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w==", "dev": true }, "node_modules/@types/node": { - "version": "20.8.9", - "resolved": "https://registry.npmjs.org/@types/node/-/node-20.8.9.tgz", - "integrity": "sha512-UzykFsT3FhHb1h7yD4CA4YhBHq545JC0YnEz41xkipN88eKQtL6rSgocL5tbAP6Ola9Izm/Aw4Ora8He4x0BHg==", + "version": "20.9.2", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.9.2.tgz", + "integrity": "sha512-WHZXKFCEyIUJzAwh3NyyTHYSR35SevJ6mZ1nWwJafKtiQbqRTIKSRcw3Ma3acqgsent3RRDqeVwpHntMk+9irg==", "dev": true, "dependencies": { "undici-types": "~5.26.4" } }, "node_modules/@types/node-forge": { - "version": "1.3.8", - "resolved": "https://registry.npmjs.org/@types/node-forge/-/node-forge-1.3.8.tgz", - "integrity": "sha512-vGXshY9vim9CJjrpcS5raqSjEfKlJcWy2HNdgUasR66fAnVEYarrf1ULV4nfvpC1nZq/moA9qyqBcu83x+Jlrg==", + "version": "1.3.9", + "resolved": "https://registry.npmjs.org/@types/node-forge/-/node-forge-1.3.9.tgz", + "integrity": "sha512-meK88cx/sTalPSLSoCzkiUB4VPIFHmxtXm5FaaqRDqBX2i/Sy8bJ4odsan0b20RBjPh06dAQ+OTTdnyQyhJZyQ==", "dev": true, "dependencies": { "@types/node": "*" } }, "node_modules/@types/qs": { - "version": "6.9.9", - "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.9.9.tgz", - "integrity": "sha512-wYLxw35euwqGvTDx6zfY1vokBFnsK0HNrzc6xNHchxfO2hpuRg74GbkEW7e3sSmPvj0TjCDT1VCa6OtHXnubsg==", + "version": "6.9.10", + "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.9.10.tgz", + "integrity": "sha512-3Gnx08Ns1sEoCrWssEgTSJs/rsT2vhGP+Ja9cnnk9k4ALxinORlQneLXFeFKOTJMOeZUFD1s7w+w2AphTpvzZw==", "dev": true }, "node_modules/@types/range-parser": { - "version": "1.2.6", - "resolved": "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.6.tgz", - "integrity": "sha512-+0autS93xyXizIYiyL02FCY8N+KkKPhILhcUSA276HxzreZ16kl+cmwvV2qAM/PuCCwPXzOXOWhiPcw20uSFcA==", + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.7.tgz", + "integrity": "sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ==", "dev": true }, "node_modules/@types/retry": { @@ -245,9 +245,9 @@ "dev": true }, "node_modules/@types/send": { - "version": "0.17.3", - "resolved": "https://registry.npmjs.org/@types/send/-/send-0.17.3.tgz", - "integrity": "sha512-/7fKxvKUoETxjFUsuFlPB9YndePpxxRAOfGC/yJdc9kTjTeP5kRCTzfnE8kPUKCeyiyIZu0YQ76s50hCedI1ug==", + "version": "0.17.4", + "resolved": "https://registry.npmjs.org/@types/send/-/send-0.17.4.tgz", + "integrity": "sha512-x2EM6TJOybec7c52BX0ZspPodMsQUd5L6PRwOunVyVUhXiBSKf3AezDL8Dgvgt5o0UfKNfuA0eMLr2wLT4AiBA==", "dev": true, "dependencies": { "@types/mime": "^1", @@ -255,18 +255,18 @@ } }, "node_modules/@types/serve-index": { - "version": "1.9.3", - "resolved": "https://registry.npmjs.org/@types/serve-index/-/serve-index-1.9.3.tgz", - "integrity": "sha512-4KG+yMEuvDPRrYq5fyVm/I2uqAJSAwZK9VSa+Zf+zUq9/oxSSvy3kkIqyL+jjStv6UCVi8/Aho0NHtB1Fwosrg==", + "version": "1.9.4", + "resolved": "https://registry.npmjs.org/@types/serve-index/-/serve-index-1.9.4.tgz", + "integrity": "sha512-qLpGZ/c2fhSs5gnYsQxtDEq3Oy8SXPClIXkW5ghvAvsNuVSA8k+gCONcUCS/UjLEYvYps+e8uBtfgXgvhwfNug==", "dev": true, "dependencies": { "@types/express": "*" } }, "node_modules/@types/serve-static": { - "version": "1.15.4", - "resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.15.4.tgz", - "integrity": "sha512-aqqNfs1XTF0HDrFdlY//+SGUxmdSUbjeRXb5iaZc3x0/vMbYmdw9qvOgHWOyyLFxSSRnUuP5+724zBgfw8/WAw==", + "version": "1.15.5", + "resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.15.5.tgz", + "integrity": "sha512-PDRk21MnK70hja/YF8AHfC7yIsiQHn1rcXx7ijCFBX/k+XQJhQT/gw3xekXKJvx+5SXaMMS8oqQy09Mzvz2TuQ==", "dev": true, "dependencies": { "@types/http-errors": "*", @@ -275,18 +275,18 @@ } }, "node_modules/@types/sockjs": { - "version": "0.3.35", - "resolved": "https://registry.npmjs.org/@types/sockjs/-/sockjs-0.3.35.tgz", - "integrity": "sha512-tIF57KB+ZvOBpAQwSaACfEu7htponHXaFzP7RfKYgsOS0NoYnn+9+jzp7bbq4fWerizI3dTB4NfAZoyeQKWJLw==", + "version": "0.3.36", + "resolved": "https://registry.npmjs.org/@types/sockjs/-/sockjs-0.3.36.tgz", + "integrity": "sha512-MK9V6NzAS1+Ud7JV9lJLFqW85VbC9dq3LmwZCuBe4wBDgKC0Kj/jd8Xl+nSviU+Qc3+m7umHHyHg//2KSa0a0Q==", "dev": true, "dependencies": { "@types/node": "*" } }, "node_modules/@types/ws": { - "version": "8.5.8", - "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.5.8.tgz", - "integrity": "sha512-flUksGIQCnJd6sZ1l5dqCEG/ksaoAg/eUwiLAGTJQcfgvZJKF++Ta4bJA6A5aPSJmsr+xlseHn4KLgVlNnvPTg==", + "version": "8.5.9", + "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.5.9.tgz", + "integrity": "sha512-jbdrY0a8lxfdTp/+r7Z4CkycbOFN8WX+IOchLJr3juT/xzbJ8URyTVSJ/hvNdadTgM1mnedb47n+Y31GsFnQlg==", "dev": true, "dependencies": { "@types/node": "*" @@ -836,9 +836,9 @@ } }, "node_modules/caniuse-lite": { - "version": "1.0.30001558", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001558.tgz", - "integrity": "sha512-/Et7DwLqpjS47JPEcz6VnxU9PwcIdVi0ciLXRWBQdj1XFye68pSQYpV0QtPTfUKWuOaEig+/Vez2l74eDc1tPQ==", + "version": "1.0.30001563", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001563.tgz", + "integrity": "sha512-na2WUmOxnwIZtwnFI2CZ/3er0wdNzU7hN+cPYz/z2ajHThnkWjNBOpEPP4n+4r2WPM847JaMotaJE3bnfzjyKw==", "dev": true, "funding": [ { @@ -1278,9 +1278,9 @@ "dev": true }, "node_modules/electron-to-chromium": { - "version": "1.4.569", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.569.tgz", - "integrity": "sha512-LsrJjZ0IbVy12ApW3gpYpcmHS3iRxH4bkKOW98y1/D+3cvDUWGcbzbsFinfUS8knpcZk/PG/2p/RnkMCYN7PVg==", + "version": "1.4.588", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.588.tgz", + "integrity": "sha512-soytjxwbgcCu7nh5Pf4S2/4wa6UIu+A3p03U2yVr53qGxi1/VTR3ENI+p50v+UxqqZAfl48j3z55ud7VHIOr9w==", "dev": true }, "node_modules/encodeurl": { @@ -1315,9 +1315,9 @@ } }, "node_modules/envinfo": { - "version": "7.10.0", - "resolved": "https://registry.npmjs.org/envinfo/-/envinfo-7.10.0.tgz", - "integrity": "sha512-ZtUjZO6l5mwTHvc1L9+1q5p/R3wTopcfqMW8r5t8SJSKqeVI/LtajORwRFEKpEFuekjD0VBjwu1HMxL4UalIRw==", + "version": "7.11.0", + "resolved": "https://registry.npmjs.org/envinfo/-/envinfo-7.11.0.tgz", + "integrity": "sha512-G9/6xF1FPbIw0TtalAMaVPpiq2aDEuKLXM314jPVAO9r2fo2a4BLqMNkmRS7O/xPPZ+COAhGIz3ETvHEV3eUcg==", "dev": true, "bin": { "envinfo": "dist/cli.js" @@ -1327,9 +1327,9 @@ } }, "node_modules/es-module-lexer": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.3.1.tgz", - "integrity": "sha512-JUFAyicQV9mXc3YRxPnDlrfBKpqt6hUYzz9/boprUJHs4e4KVr3XwOF70doO6gwXUor6EWZJAyWAfKki84t20Q==", + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.4.1.tgz", + "integrity": "sha512-cXLGjP0c4T3flZJKQSuziYoq7MlT+rnvfZjfp7h+I7K9BNX54kP9nyWvdbwjQ4u1iWbOL4u96fgeZLToQlZC7w==", "dev": true }, "node_modules/escalade": { @@ -2845,9 +2845,9 @@ } }, "node_modules/punycode": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.0.tgz", - "integrity": "sha512-rRV+zQD8tVFys26lAGR9WUuS4iUAngJScM+ZRSKtvl5tKeZ2t5bvdNFdNHBW9FWR4guGHlgmsZ1G7BSm2wTbuA==", + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", "dev": true, "engines": { "node": ">=6" @@ -3521,9 +3521,9 @@ } }, "node_modules/terser": { - "version": "5.23.0", - "resolved": "https://registry.npmjs.org/terser/-/terser-5.23.0.tgz", - "integrity": "sha512-Iyy83LN0uX9ZZLCX4Qbu5JiHiWjOCTwrmM9InWOzVeM++KNWEsqV4YgN9U9E8AlohQ6Gs42ztczlWOG/lwDAMA==", + "version": "5.24.0", + "resolved": "https://registry.npmjs.org/terser/-/terser-5.24.0.tgz", + "integrity": "sha512-ZpGR4Hy3+wBEzVEnHvstMvqpD/nABNelQn/z2r0fjVWGQsN3bpOLzQlqDxmb4CDZnXq5lpjnQ+mHQLAOpfM5iw==", "dev": true, "dependencies": { "@jridgewell/source-map": "^0.3.3", @@ -3613,9 +3613,9 @@ } }, "node_modules/ts-loader": { - "version": "9.5.0", - "resolved": "https://registry.npmjs.org/ts-loader/-/ts-loader-9.5.0.tgz", - "integrity": "sha512-LLlB/pkB4q9mW2yLdFMnK3dEHbrBjeZTYguaaIfusyojBgAGf5kF+O6KcWqiGzWqHk0LBsoolrp4VftEURhybg==", + "version": "9.5.1", + "resolved": "https://registry.npmjs.org/ts-loader/-/ts-loader-9.5.1.tgz", + "integrity": "sha512-rNH3sK9kGZcH9dYzC7CewQm4NtxJTjSEVRJ2DyBZR7f8/wcta+iV44UPCXc5+nzDzivKtlzV6c9P4e+oFhDLYg==", "dev": true, "dependencies": { "chalk": "^4.1.0", diff --git a/languages/js_webassembly/package.json b/languages/js_webassembly/package.json index 95d7002a5..d5d7342df 100644 --- a/languages/js_webassembly/package.json +++ b/languages/js_webassembly/package.json @@ -6,7 +6,7 @@ "devDependencies": { "html-webpack-plugin": "5.5.3", "text-encoding": "0.7.0", - "ts-loader": "9.5.0", + "ts-loader": "9.5.1", "wasm-pack": "0.12.1", "webpack": "5.89.0", "webpack-cli": "5.1.4", diff --git a/languages/kotlin/app/build.gradle b/languages/kotlin/app/build.gradle index 709e32889..ef42d43bb 100644 --- a/languages/kotlin/app/build.gradle +++ b/languages/kotlin/app/build.gradle @@ -60,6 +60,7 @@ dependencies { implementation 'androidx.compose.ui:ui-graphics' implementation 'androidx.compose.ui:ui-tooling-preview' implementation 'androidx.compose.material3:material3' + implementation "androidx.biometric:biometric:1.1.0" implementation "io.ktor:ktor-client-core:2.3.3" implementation "io.ktor:ktor-client-cio:2.3.3" implementation "io.ktor:ktor-client-content-negotiation:2.3.3" diff --git a/languages/kotlin/app/src/main/java/com/bitwarden/myapplication/Biometrics.kt b/languages/kotlin/app/src/main/java/com/bitwarden/myapplication/Biometrics.kt new file mode 100644 index 000000000..8f8115e8d --- /dev/null +++ b/languages/kotlin/app/src/main/java/com/bitwarden/myapplication/Biometrics.kt @@ -0,0 +1,132 @@ +package com.bitwarden.myapplication + +import android.os.Build +import android.security.keystore.KeyGenParameterSpec +import android.security.keystore.KeyProperties +import android.util.Log +import androidx.biometric.BiometricManager +import androidx.biometric.BiometricManager.Authenticators +import androidx.biometric.BiometricPrompt +import androidx.biometric.BiometricPrompt.CryptoObject +import androidx.core.content.ContextCompat +import androidx.fragment.app.FragmentActivity +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.async +import java.security.KeyStore +import java.util.Base64 +import javax.crypto.Cipher +import javax.crypto.KeyGenerator +import javax.crypto.SecretKey +import javax.crypto.spec.GCMParameterSpec + +/** + * IMPORTANT: This file is provided only for the purpose of demostrating the use of the biometric unlock functionality. + * It hasn't gone through a throrough security review and should not be considered production ready. It also doesn't + * handle a lot of errors and edge cases that a production application would need to deal with. + * Developers are encouraged to review and improve the code as needed to meet their security requirements. + * Additionally, we recommend to consult with security experts and conduct thorough testing before using the code in production. + */ + +class Biometric(private var activity: FragmentActivity) { + private var promptInfo: BiometricPrompt.PromptInfo = + BiometricPrompt.PromptInfo.Builder().setTitle("Unlock") + .setSubtitle("Bitwarden biometric unlock") + .setDescription("Confirm biometric to continue").setConfirmationRequired(true) + .setNegativeButtonText("Use account password").build() + + suspend fun encryptString( + keyName: String, plaintext: String, callback: (String, String) -> Unit + ) { + if (canAuthenticate()) { + val cipher = getCipher() + cipher.init(Cipher.ENCRYPT_MODE, getSecretKey(keyName)) + + val bio = createBiometricPrompt { + val ciphertext = it.cipher!!.doFinal(plaintext.toByteArray()) + callback( + String(Base64.getEncoder().encode(ciphertext)), + String(Base64.getEncoder().encode(cipher.iv)) + ) + } + CoroutineScope(Dispatchers.Main).async { + bio.authenticate(promptInfo, CryptoObject(cipher)) + }.await() + } + } + + suspend fun decryptString( + keyName: String, encrypted: String, initializationVector: String, callback: (String) -> Unit + ) { + if (canAuthenticate()) { + val enc = Base64.getDecoder().decode(encrypted) + val iv = Base64.getDecoder().decode(initializationVector) + + val cipher = getCipher() + cipher.init(Cipher.DECRYPT_MODE, getSecretKey(keyName), GCMParameterSpec(128, iv)) + + val bio = createBiometricPrompt { + callback(String(it.cipher!!.doFinal(enc))) + } + + CoroutineScope(Dispatchers.Main).async { + bio.authenticate(promptInfo, CryptoObject(cipher)) + }.await() + } + } + + private fun canAuthenticate() = BiometricManager.from(activity) + .canAuthenticate(Authenticators.BIOMETRIC_STRONG) == BiometricManager.BIOMETRIC_SUCCESS + + private fun createBiometricPrompt(processData: (CryptoObject) -> Unit): BiometricPrompt { + return BiometricPrompt(activity, + ContextCompat.getMainExecutor(activity), + object : BiometricPrompt.AuthenticationCallback() { + override fun onAuthenticationError(errorCode: Int, errString: CharSequence) { + super.onAuthenticationError(errorCode, errString) + Log.e("Biometric", "$errorCode :: $errString") + } + + override fun onAuthenticationFailed() { + super.onAuthenticationFailed() + Log.e("Biometric", "Authentication failed for an unknown reason") + } + + override fun onAuthenticationSucceeded(result: BiometricPrompt.AuthenticationResult) { + super.onAuthenticationSucceeded(result) + processData(result.cryptoObject!!) + } + }) + } + + private fun getCipher(): Cipher { + val transform = + "${KeyProperties.KEY_ALGORITHM_AES}/${KeyProperties.BLOCK_MODE_GCM}/${KeyProperties.ENCRYPTION_PADDING_NONE}" + return Cipher.getInstance(transform) + } + + private fun getSecretKey(keyName: String): SecretKey { + // If the SecretKey exists, return it + val keyStore = KeyStore.getInstance("AndroidKeyStore") + keyStore.load(null) + keyStore.getKey(keyName, null)?.let { return it as SecretKey } + + // Otherwise, we generate a new one + val keyGenParams = KeyGenParameterSpec.Builder( + keyName, KeyProperties.PURPOSE_ENCRYPT or KeyProperties.PURPOSE_DECRYPT + ).apply { + setBlockModes(KeyProperties.BLOCK_MODE_GCM) + setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_NONE) + setKeySize(256) + setUserAuthenticationRequired(true) + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { + setUserAuthenticationParameters(0, KeyProperties.AUTH_BIOMETRIC_STRONG) + } + }.build() + + val keyGenerator = + KeyGenerator.getInstance(KeyProperties.KEY_ALGORITHM_AES, "AndroidKeyStore") + keyGenerator.init(keyGenParams) + return keyGenerator.generateKey() + } +} diff --git a/languages/kotlin/app/src/main/java/com/bitwarden/myapplication/MainActivity.kt b/languages/kotlin/app/src/main/java/com/bitwarden/myapplication/MainActivity.kt index 2d204e075..3c03c6bca 100644 --- a/languages/kotlin/app/src/main/java/com/bitwarden/myapplication/MainActivity.kt +++ b/languages/kotlin/app/src/main/java/com/bitwarden/myapplication/MainActivity.kt @@ -1,18 +1,32 @@ package com.bitwarden.myapplication +import android.content.Context import android.os.Bundle -import androidx.activity.ComponentActivity import androidx.activity.compose.setContent +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.Button +import androidx.compose.material3.Checkbox +import androidx.compose.material3.Divider import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Surface import androidx.compose.material3.Text -import androidx.compose.runtime.Composable +import androidx.compose.runtime.MutableState +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.ui.Alignment +import androidx.compose.ui.Alignment.Companion.CenterVertically import androidx.compose.ui.Modifier -import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.unit.dp +import androidx.fragment.app.FragmentActivity import com.bitwarden.core.DateTime import com.bitwarden.core.Folder -import com.bitwarden.core.InitCryptoRequest +import com.bitwarden.core.InitOrgCryptoRequest +import com.bitwarden.core.InitUserCryptoMethod +import com.bitwarden.core.InitUserCryptoRequest import com.bitwarden.core.Kdf import com.bitwarden.core.Uuid import com.bitwarden.myapplication.ui.theme.MyApplicationTheme @@ -42,144 +56,272 @@ import java.security.cert.X509Certificate import java.util.Base64 import javax.net.ssl.X509TrustManager -class MainActivity : ComponentActivity() { +/** + * IMPORTANT: This file is provided only for the purpose of demostrating the use of the SDK functionality. + * It hasn't gone through a throrough security review and should not be considered production ready. It also doesn't + * handle a lot of errors and edge cases that a production application would need to deal with. + * Developers are encouraged to review and improve the code as needed to meet their security requirements. + * Additionally, we recommend to consult with security experts and conduct thorough testing before using the code in production. + */ + +const val SERVER_URL = "https://10.0.2.2:8080/" +const val API_URL = SERVER_URL + "api/" +const val IDENTITY_URL = SERVER_URL + "identity/" + +const val EMAIL = "test@bitwarden.com" +const val PASSWORD = "asdfasdfasdf" + +// We should separate keys for each user by appending the user_id +const val BIOMETRIC_KEY = "biometric_key" + +class MainActivity : FragmentActivity() { + private lateinit var biometric: Biometric + private lateinit var client: Client + private lateinit var http: HttpClient + + private var accessToken = "" + override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) + biometric = Biometric(this) + client = Client(null) + http = httpClient() + + setContent { + MyApplicationTheme { + // A surface container using the 'background' color from the theme + Surface( + modifier = Modifier.fillMaxSize(), color = MaterialTheme.colorScheme.background + ) { + Column( + horizontalAlignment = Alignment.CenterHorizontally + ) { - val SERVER_URL = "https://10.0.2.2:8080/" - val API_URL = SERVER_URL + "api/" - val IDENTITY_URL = SERVER_URL + "identity/" + val setupBiometrics = remember { mutableStateOf(true) } + val outputText = remember { mutableStateOf("") } - val EMAIL = "test@bitwarden.com" - val PASSWORD = "asdfasdfasdf" + Row { + Checkbox(checked = setupBiometrics.value, + onCheckedChange = { isChecked -> + setupBiometrics.value = isChecked + }) + Text( + "Setup biometric unlock after login", + modifier = Modifier.align(CenterVertically) + ) + } - GlobalScope.launch { - var client = Client(null) - val http = httpClient() + Button({ + GlobalScope.launch { + clientExamplePassword( + client, http, outputText, setupBiometrics.value + ) + } + }) { + Text("Login with username + password") + } - ///////////////////////////// Get master password hash ///////////////////////////// - @Serializable - data class PreloginRequest(val email: String) + Divider( + color = Color.Black, + thickness = 1.dp, + modifier = Modifier.padding(30.dp) + ) - @Serializable - data class PreloginResponse( - val kdf: UInt, - val kdfIterations: UInt, - val kdfMemory: UInt?, - val kdfParallelism: UInt? - ) + Button({ + GlobalScope.launch { + clientExampleBiometrics(client, http, outputText) + } + }) { + Text("Unlock with biometrics") + } - val prelogin_body = http.post(IDENTITY_URL + "accounts/prelogin") { - contentType(ContentType.Application.Json) - setBody(PreloginRequest(EMAIL)) - }.body() - val kdf = if (prelogin_body.kdf == 0u) { - Kdf.Pbkdf2(prelogin_body.kdfIterations) - } else { - Kdf.Argon2id( - prelogin_body.kdfIterations, - prelogin_body.kdfMemory!!, - prelogin_body.kdfParallelism!! - ) + Button({ + GlobalScope.launch { + client.destroy() + client = Client(null) + outputText.value = "OK" + } + }) { + Text("Lock & reset client") + } + + Text( + "Output: " + outputText.value, + modifier = Modifier.padding(vertical = 10.dp) + ) + } + } } - val masterPasswordHash = client.auth().hashPassword(EMAIL, PASSWORD, kdf) + } + } + + private suspend fun clientExamplePassword( + client: Client, http: HttpClient, outputText: MutableState, setupBiometrics: Boolean + ) { + println("### Logging in with username and password ###") + ///////////////////////////// Get master password hash ///////////////////////////// + @Serializable + data class PreloginRequest(val email: String) - ///////////////////////////// Login ///////////////////////////// + @Serializable + data class PreloginResponse( + val kdf: UInt, val kdfIterations: UInt, val kdfMemory: UInt?, val kdfParallelism: UInt? + ) - @Serializable - data class LoginResponse( - val Key: String, - val PrivateKey: String, - val access_token: String, - val refresh_token: String, + val prelogin_body = http.post(IDENTITY_URL + "accounts/prelogin") { + contentType(ContentType.Application.Json) + setBody(PreloginRequest(EMAIL)) + }.body() + val kdf = if (prelogin_body.kdf == 0u) { + Kdf.Pbkdf2(prelogin_body.kdfIterations) + } else { + Kdf.Argon2id( + prelogin_body.kdfIterations, + prelogin_body.kdfMemory!!, + prelogin_body.kdfParallelism!! ) + } + val masterPasswordHash = client.auth().hashPassword(EMAIL, PASSWORD, kdf) - val loginBody = http.post(IDENTITY_URL + "connect/token") { - contentType(ContentType.Application.Json) - header("Auth-Email", Base64.getEncoder().encodeToString(EMAIL.toByteArray())) - setBody(FormDataContent(Parameters.build { - append("scope", "api offline_access") - append("client_id", "web") - append("deviceType", "12") - append("deviceIdentifier", "0745d426-8dab-484a-9816-4959721d77c7") - append("deviceName", "edge") - - append("grant_type", "password") - append("username", EMAIL) - append("password", masterPasswordHash) - })) - }.body() - - ///////////////////////////// Sync ///////////////////////////// - - val syncBody = http.get(API_URL + "sync?excludeDomains=true") { - bearerAuth(loginBody.access_token) - }.body() - - val folders = (syncBody["folders"] as JsonArray).map { - val o = it as JsonObject; - Folder( - (o["id"] as JsonPrimitive).content, - (o["name"] as JsonPrimitive).content, - DateTime.parse( - (o["revisionDate"] as JsonPrimitive).content - ) - ) - } + ///////////////////////////// Login ///////////////////////////// - ///////////////////////////// Initialize crypto ///////////////////////////// - val orgs = ((syncBody["profile"] as JsonObject)["organizations"]) as JsonArray - val orgKeys = HashMap() + @Serializable + data class LoginResponse( + val Key: String, + val PrivateKey: String, + val access_token: String, + val refresh_token: String, + ) - for (org in orgs) { - val o = org as JsonObject - orgKeys[(o["id"] as JsonPrimitive).content] = (o["key"] as JsonPrimitive).content - } + val loginBody = http.post(IDENTITY_URL + "connect/token") { + contentType(ContentType.Application.Json) + header("Auth-Email", Base64.getEncoder().encodeToString(EMAIL.toByteArray())) + setBody(FormDataContent(Parameters.build { + append("scope", "api offline_access") + append("client_id", "web") + append("deviceType", "12") + append("deviceIdentifier", "0745d426-8dab-484a-9816-4959721d77c7") + append("deviceName", "edge") + + append("grant_type", "password") + append("username", EMAIL) + append("password", masterPasswordHash) + })) + }.body() - client.crypto().initializeCrypto( - InitCryptoRequest( - kdfParams = kdf, - email = EMAIL, - password = PASSWORD, - userKey = loginBody.Key, - privateKey = loginBody.PrivateKey, - organizationKeys = orgKeys + client.crypto().initializeUserCrypto( + InitUserCryptoRequest( + kdfParams = kdf, + email = EMAIL, + privateKey = loginBody.PrivateKey, + method = InitUserCryptoMethod.Password( + password = PASSWORD, userKey = loginBody.Key ) ) + ) - ///////////////////////////// Decrypt some folders ///////////////////////////// + accessToken = loginBody.access_token - val decryptedFolders = client.vault().folders().decryptList(folders) + decryptVault(client, http, outputText) - println(decryptedFolders) + if (setupBiometrics) { + // Save values for future logins + val sharedPref = getPreferences(Context.MODE_PRIVATE) + with(sharedPref.edit()) { + putString("accessToken", accessToken) + putString("privateKey", loginBody.PrivateKey) - } + putInt("kdfType", prelogin_body.kdf.toInt()) + putInt("kdfIterations", prelogin_body.kdfIterations.toInt()) + putInt("kdfMemory", (prelogin_body.kdfMemory ?: 0u).toInt()) + putInt("kdfParallelism", (prelogin_body.kdfParallelism ?: 0u).toInt()) - setContent { - MyApplicationTheme { - // A surface container using the 'background' color from the theme - Surface( - modifier = Modifier.fillMaxSize(), color = MaterialTheme.colorScheme.background - ) { - Greeting("Hey") + // TODO: This should be protected by Android's secure KeyStore + val decryptedKey = client.crypto().getUserEncryptionKey() + + biometric.encryptString(BIOMETRIC_KEY, decryptedKey) { key, iv -> + putString("encryptedUserKey", key) + putString("encryptedUserKeyIv", iv) + apply() } } } } -} -@Composable -fun Greeting(name: String, modifier: Modifier = Modifier) { - Text( - text = "Hello $name!", modifier = modifier - ) -} + private suspend fun clientExampleBiometrics( + client: Client, http: HttpClient, outputText: MutableState + ) { + println("### Unlocking with Biometrics ###") + + val pref = getPreferences(Context.MODE_PRIVATE) + accessToken = pref.getString("accessToken", "").orEmpty() + val privateKey = pref.getString("privateKey", "") + + val kdf = if (pref.getInt("kdfType", 0) == 0) { + Kdf.Pbkdf2(pref.getInt("kdfIterations", 0).toUInt()) + } else { + Kdf.Argon2id( + pref.getInt("kdfIterations", 0).toUInt(), + pref.getInt("kdfMemory", 0).toUInt(), + pref.getInt("kdfParallelism", 0).toUInt() + ) + } + + val encryptedUserKey = pref.getString("encryptedUserKey", "")!! + val encryptedUserKeyIv = pref.getString("encryptedUserKeyIv", "")!! + + biometric.decryptString( + BIOMETRIC_KEY, encryptedUserKey, encryptedUserKeyIv + ) { key -> + GlobalScope.launch { + client.crypto().initializeUserCrypto( + InitUserCryptoRequest( + kdfParams = kdf, + email = EMAIL, + privateKey = privateKey!!, + method = InitUserCryptoMethod.DecryptedKey(decryptedUserKey = key) + ) + ) + + decryptVault(client, http, outputText) + } + } + } + + suspend fun decryptVault(client: Client, http: HttpClient, outputText: MutableState) { + ///////////////////////////// Sync ///////////////////////////// + + val syncBody = http.get(API_URL + "sync?excludeDomains=true") { + bearerAuth(accessToken) + }.body() + + val folders = (syncBody["folders"] as JsonArray).map { + val o = it as JsonObject + Folder( + (o["id"] as JsonPrimitive).content, + (o["name"] as JsonPrimitive).content, + DateTime.parse( + (o["revisionDate"] as JsonPrimitive).content + ) + ) + } + + ///////////////////////////// Initialize org crypto ///////////////////////////// + val orgs = ((syncBody["profile"] as JsonObject)["organizations"]) as JsonArray + val orgKeys = HashMap() + + for (org in orgs) { + val o = org as JsonObject + orgKeys[(o["id"] as JsonPrimitive).content] = (o["key"] as JsonPrimitive).content + } + + client.crypto().initializeOrgCrypto(InitOrgCryptoRequest(organizationKeys = orgKeys)) + + ///////////////////////////// Decrypt some folders ///////////////////////////// -@Preview(showBackground = true) -@Composable -fun GreetingPreview() { - MyApplicationTheme { - Greeting("Sdk") + val decryptedFolders = client.vault().folders().decryptList(folders) + outputText.value = decryptedFolders.toString() + println(decryptedFolders) } } diff --git a/languages/kotlin/doc.md b/languages/kotlin/doc.md index 6174df750..652e09813 100644 --- a/languages/kotlin/doc.md +++ b/languages/kotlin/doc.md @@ -182,17 +182,40 @@ Decrypt collection list ## ClientCrypto -### `initialize_crypto` +### `initialize_user_crypto` -Initialization method for the crypto. Needs to be called before any other crypto operations. +Initialization method for the user crypto. Needs to be called before any other crypto operations. **Arguments**: - self: -- req: [InitCryptoRequest](#initcryptorequest) +- req: [InitUserCryptoRequest](#initusercryptorequest) **Output**: std::result::Result<,BitwardenError> +### `initialize_org_crypto` + +Initialization method for the organization crypto. Needs to be called after +`initialize_user_crypto` but before any other crypto operations. + +**Arguments**: + +- self: +- req: [InitOrgCryptoRequest](#initorgcryptorequest) + +**Output**: std::result::Result<,BitwardenError> + +### `get_user_encryption_key` + +Get the uses's decrypted encryption key. Note: It's very important to keep this key safe, +as it can be used to decrypt all of the user's data + +**Arguments**: + +- self: + +**Output**: std::result::Result + ## ClientExporters ### `export_vault` @@ -304,6 +327,19 @@ Decrypt password history **Output**: std::result::Result +## ClientPlatform + +### `fingerprint` + +Fingerprint + +**Arguments**: + +- self: +- req: [FingerprintRequest](#fingerprintrequest) + +**Output**: std::result::Result + ## ClientSends ### `encrypt` @@ -492,6 +528,11 @@ implementations. array + + key + + More recent ciphers uses individual encryption keys to encrypt the other fields of the Cipher. + name @@ -617,6 +658,11 @@ implementations. array + + key + + + name string @@ -785,6 +831,26 @@ implementations. +## `FingerprintRequest` + + + + + + + + + + + + + + + + + +
KeyTypeDescription
fingerprintMaterialstringThe input material, used in the fingerprint generation process.
publicKeystringThe user's public key encoded with base64.
+ ## `Folder` @@ -835,7 +901,7 @@ implementations.
-## `InitCryptoRequest` +## `InitOrgCryptoRequest` @@ -844,34 +910,96 @@ implementations. - - - + + + +
Description
kdfParamsThe user's KDF parameters, as received from the prelogin requestorganizationKeysobjectThe encryption keys for all the organizations the user is a part of
+ +## `InitUserCryptoMethod` + + - - - + + + - - + + + + + + + + + + + + + +
emailstringThe user's email addressKeyTypeDescription
passwordstringThe user's master passwordobject
+ + + + + + + + + + + + + + + + +
KeyTypeDescription
passwordstringThe user's master password
user_keystringThe user's encrypted symmetric crypto key
+
decryptedKeyobject
+ + + + + + + + + + + +
KeyTypeDescription
decrypted_user_keystringThe user's decrypted encryption key, obtained using `get_user_encryption_key`
+
+ +## `InitUserCryptoRequest` + + - + + + + + + + + + + + - + - + - - - + + +
userKeyKeyTypeDescription
kdfParamsThe user's KDF parameters, as received from the prelogin request
email stringThe user's encrypted symmetric crypto keyThe user's email address
privateKey stringThe user's encryptred private keyThe user's encrypted private key
organizationKeysobjectThe encryption keys for all the organizations the user is a part ofmethodThe initialization method to use
diff --git a/languages/python/BitwardenClient/bitwarden_client.py b/languages/python/BitwardenClient/bitwarden_client.py index 6a0a77ed8..cb669a40e 100644 --- a/languages/python/BitwardenClient/bitwarden_client.py +++ b/languages/python/BitwardenClient/bitwarden_client.py @@ -1,8 +1,8 @@ import json -from typing import Any, List +from typing import Any, List, Optional +from uuid import UUID import bitwarden_py -from .schemas import ClientSettings, Command, PasswordLoginRequest, PasswordLoginResponse, ResponseForPasswordLoginResponse, ResponseForSecretIdentifiersResponse, ResponseForSecretResponse, ResponseForSecretsDeleteResponse, ResponseForSyncResponse, ResponseForUserAPIKeyResponse, SecretCreateRequest, SecretGetRequest, SecretIdentifiersRequest, SecretIdentifiersResponse, SecretPutRequest, SecretResponse, SecretVerificationRequest, SecretsCommand, SecretsDeleteRequest, SecretsDeleteResponse, SyncRequest, SyncResponse, UserAPIKeyResponse - +from .schemas import ClientSettings, Command, ResponseForSecretIdentifiersResponse, ResponseForSecretResponse, ResponseForSecretsDeleteResponse, SecretCreateRequest, SecretGetRequest, SecretIdentifiersRequest, SecretIdentifiersResponse, SecretPutRequest, SecretResponse, SecretsCommand, SecretsDeleteRequest, SecretsDeleteResponse, AccessTokenLoginRequest, AccessTokenLoginResponse, ResponseForAccessTokenLoginResponse, ResponseForProjectResponse, ProjectsCommand, ProjectCreateRequest, ProjectGetRequest, ProjectPutRequest, ProjectsListRequest, ResponseForProjectsResponse, ResponseForProjectsDeleteResponse, ProjectsDeleteRequest class BitwardenClient: def __init__(self, settings: ClientSettings = None): @@ -12,32 +12,25 @@ def __init__(self, settings: ClientSettings = None): settings_json = json.dumps(settings.to_dict()) self.inner = bitwarden_py.BitwardenClient(settings_json) - def password_login(self, email: str, password: str) -> ResponseForPasswordLoginResponse: - result = self._run_command( - Command(password_login=PasswordLoginRequest(email, password)) - ) - return ResponseForPasswordLoginResponse.from_dict(result) - - def get_user_api_key(self, secret: str, is_otp: bool = False) -> ResponseForUserAPIKeyResponse: - result = self._run_command( - Command(get_user_api_key=SecretVerificationRequest( - secret if not is_otp else None, secret if is_otp else None)) + def access_token_login(self, access_token: str): + self._run_command( + Command(access_token_login=AccessTokenLoginRequest(access_token)) ) - return ResponseForUserAPIKeyResponse.from_dict(result) - - def sync(self, exclude_subdomains: bool = False) -> ResponseForSyncResponse: - result = self._run_command( - Command(sync=SyncRequest(exclude_subdomains)) - ) - return ResponseForSyncResponse.from_dict(result) def secrets(self): return SecretsClient(self) + def projects(self): + return ProjectsClient(self) + def _run_command(self, command: Command) -> Any: response_json = self.inner.run_command(json.dumps(command.to_dict())) - return json.loads(response_json) + response = json.loads(response_json) + if response["success"] == False: + raise Exception(response["errorMessage"]) + + return response class SecretsClient: def __init__(self, client: BitwardenClient): @@ -52,10 +45,12 @@ def get(self, id: str) -> ResponseForSecretResponse: def create(self, key: str, note: str, organization_id: str, - value: str) -> ResponseForSecretResponse: + value: str, + project_ids: Optional[List[UUID]] = None + ) -> ResponseForSecretResponse: result = self.client._run_command( Command(secrets=SecretsCommand( - create=SecretCreateRequest(key, note, organization_id, value))) + create=SecretCreateRequest(key, note, organization_id, value, project_ids))) ) return ResponseForSecretResponse.from_dict(result) @@ -70,10 +65,12 @@ def update(self, id: str, key: str, note: str, organization_id: str, - value: str) -> ResponseForSecretResponse: + value: str, + project_ids: Optional[List[UUID]] = None + ) -> ResponseForSecretResponse: result = self.client._run_command( Command(secrets=SecretsCommand(update=SecretPutRequest( - id, key, note, organization_id, value))) + id, key, note, organization_id, value, project_ids))) ) return ResponseForSecretResponse.from_dict(result) @@ -82,3 +79,46 @@ def delete(self, ids: List[str]) -> ResponseForSecretsDeleteResponse: Command(secrets=SecretsCommand(delete=SecretsDeleteRequest(ids))) ) return ResponseForSecretsDeleteResponse.from_dict(result) + +class ProjectsClient: + def __init__(self, client: BitwardenClient): + self.client = client + + def get(self, id: str) -> ResponseForProjectResponse: + result = self.client._run_command( + Command(projects=ProjectsCommand(get=ProjectGetRequest(id))) + ) + return ResponseForProjectResponse.from_dict(result) + + def create(self, + name: str, + organization_id: str, + ) -> ResponseForProjectResponse: + result = self.client._run_command( + Command(projects=ProjectsCommand( + create=ProjectCreateRequest(name, organization_id))) + ) + return ResponseForProjectResponse.from_dict(result) + + def list(self, organization_id: str) -> ResponseForProjectsResponse: + result = self.client._run_command( + Command(projects=ProjectsCommand( + list=ProjectsListRequest(organization_id))) + ) + return ResponseForProjectsResponse.from_dict(result) + + def update(self, id: str, + name: str, + organization_id: str, + ) -> ResponseForProjectResponse: + result = self.client._run_command( + Command(projects=ProjectsCommand(update=ProjectPutRequest( + id, name, organization_id))) + ) + return ResponseForProjectResponse.from_dict(result) + + def delete(self, ids: List[str]) -> ResponseForProjectsDeleteResponse: + result = self.client._run_command( + Command(projects=ProjectsCommand(delete=ProjectsDeleteRequest(ids))) + ) + return ResponseForProjectsDeleteResponse.from_dict(result) diff --git a/languages/python/README.md b/languages/python/README.md index 03a4ce572..871be274f 100644 --- a/languages/python/README.md +++ b/languages/python/README.md @@ -9,6 +9,10 @@ ```bash pip install setuptools_rust ``` +- dateutil + ```bash + pip install python-dateutil + ``` # Installation @@ -18,7 +22,7 @@ From the `languages/python/` directory, python3 ./setup.py develop ``` -Move the the resulting `.so` file to `bitwarden_py.so`, if it isn't already there. +Rename the the resulting `.so` file to `bitwarden_py.so`, if it isn't already there. # Run diff --git a/languages/python/example.py b/languages/python/example.py new file mode 100644 index 000000000..2ee1cb280 --- /dev/null +++ b/languages/python/example.py @@ -0,0 +1,44 @@ +import json +import logging +import sys +from BitwardenClient.bitwarden_client import BitwardenClient +from BitwardenClient.schemas import client_settings_from_dict, DeviceType + +# Create the BitwardenClient, which is used to interact with the SDK +client = BitwardenClient(client_settings_from_dict({ + "apiUrl": "http://localhost:4000", + "deviceType": DeviceType.SDK, + "identityUrl": "http://localhost:33656", + "userAgent": "Python", +})) + +# Add some logging & set the org id +logging.basicConfig(level=logging.DEBUG) +organization_id = "org_id_here" + +# Attempt to authenticate with the Secrets Manager Access Token +client.access_token_login("access_token_here") + +# -- Example Project Commands -- + +project = client.projects().create("ProjectName", organization_id) +project2 = client.projects().create("Project - Don't Delete Me!", organization_id) +updated_project = client.projects().update(project.data.id, "Cool New Project Name", organization_id) +get_that_project = client.projects().get(project.data.id) + +input("Press Enter to delete the project...") +client.projects().delete([project.data.id]) + +print(client.projects().list(organization_id)) + +# -- Example Secret Commands -- + +secret = client.secrets().create("TEST_SECRET", "This is a test secret", organization_id, "Secret1234!", [project2.data.id]) +secret2 = client.secrets().create("Secret - Don't Delete Me!", "This is a test secret that will stay", organization_id, "Secret1234!", [project2.data.id]) +secret_updated = client.secrets().update(secret.data.id, "TEST_SECRET_UPDATED", "This as an updated test secret", organization_id, "Secret1234!_updated", [project2.data.id]) +secret_retrieved = client.secrets().get(secret.data.id) + +input("Press Enter to delete the secret...") +client.secrets().delete([secret.data.id]) + +print(client.secrets().list(organization_id)) diff --git a/languages/python/login.py b/languages/python/login.py deleted file mode 100644 index 9b756a005..000000000 --- a/languages/python/login.py +++ /dev/null @@ -1,24 +0,0 @@ -import json -import logging -from BitwardenClient.bitwarden_client import BitwardenClient -from BitwardenClient.schemas import client_settings_from_dict - -client = BitwardenClient(client_settings_from_dict({ - "api_url": "http://localhost:4000", - "identity_url": "http://localhost:33656", - "user_agent": "Python", -})) - -logging.basicConfig(level=logging.DEBUG) - -result = client.password_login("test@bitwarden.com", "asdfasdf") -print(result) -print(client.get_user_api_key("asdfasdf")) - -sync = client.sync() - -secret = client.secrets().create("TEST_SECRET", "This is a test secret", - sync.data.profile.organizations[0].id, "Secret1234!") -print(secret) - -client.secrets().delete([secret.data.id]) diff --git a/languages/ruby/.gitignore b/languages/ruby/.gitignore new file mode 100644 index 000000000..f0e7bd4c1 --- /dev/null +++ b/languages/ruby/.gitignore @@ -0,0 +1,3 @@ +*.lock +*.gem +bitwarden_sdk/lib/schemas.rb diff --git a/languages/ruby/CHANGELOG.md b/languages/ruby/CHANGELOG.md new file mode 100644 index 000000000..91c5ee3c2 --- /dev/null +++ b/languages/ruby/CHANGELOG.md @@ -0,0 +1,5 @@ +## [Unreleased] + +## [0.1.0] - 2023-09-19 + +- Initial release diff --git a/languages/ruby/README.md b/languages/ruby/README.md new file mode 100644 index 000000000..d02d5c500 --- /dev/null +++ b/languages/ruby/README.md @@ -0,0 +1,95 @@ +# Bitwarden Secrets Manager SDK + +Ruby bindings for interacting with the [Bitwarden Secrets Manager]. This is a beta release and might be missing some functionality. + +## Installation + +Requirements: Ruby >= 3.0 + +Install gem: `gem install bitwarden-sdk` + +Import it: require 'bitwarden-sdk' + + +## Usage + +To interact with client first you need to obtain access token from Bitwarden. +Client will be initialized with default client settings if they are not provided +via env variables. + +```ruby +require 'bitwarden-sdk' + +# then you can initialize BitwardenSettings: +bitwarden_settings = BitwardenSDK::BitwardenSettings.new( + 'https://api.bitwarden.com', + 'https://identity.bitwarden.com' +) + +# By passing these setting you can initialize BitwardenClient + +bw_client = BitwardenSDK::BitwardenClient.new(bitwarden_settings) +response = bw_client.access_token_login(token) +puts response +``` + +After successful authorization you can interact with client to manage your projects and secrets. +```ruby + +# CREATE project +project_name = 'Test project 1' +response = bw_client.project_client.create_project(project_name, organization_id) +puts response +project_id = response['id'] + +# GET project +response = bw_client.project_client.get(project_id) +puts response + +# LIST projects +response = bw_client.project_client.list_projects(organization_id) +puts response + +# UPDATE projects +name = 'Updated test project 1' +response = bw_client.project_client.update_project(project_id, name, organization_id) +puts response + +# DELETE project +response = bw_client.project_client.delete_projects([project_id]) +puts response +``` + +Similarly, you interact with secrets: +```ruby +# CREATE secret +key = 'AWS-SES' +note = 'Private account' +value = '8t27.dfj;' +response = bw_client.secrets_client.create(key, note, organization_id, [project_id], value) +puts response +secret_id = response['id'] + +# GET secret +response = bw_client.secrets_client.get(secret_id) +puts response + +# GET secret by ids +response = bw_client.secrets_client.get_by_ids([secret_id]) +puts response + +# LIST secrets +response = bw_client.secrets_client.list(organization_id) +puts response + +# UPDATE secret +note = 'updated password' +value = '7I.ert10AjK' +response = bw_client.secrets_client.update(secret_id, key, note,organization_id, [project_id], value) +puts response + +# DELETE secret +response = bw_client.secrets_client.delete_secret([secret_id]) +puts response +``` +[Bitwarden Secrets Manager]: https://bitwarden.com/products/secrets-manager/ diff --git a/languages/ruby/bitwarden_sdk/Rakefile b/languages/ruby/bitwarden_sdk/Rakefile new file mode 100644 index 000000000..192414345 --- /dev/null +++ b/languages/ruby/bitwarden_sdk/Rakefile @@ -0,0 +1,8 @@ +# frozen_string_literal: true + +require "bundler/gem_tasks" +require "rubocop/rake_task" + +RuboCop::RakeTask.new + +task default: :rubocop diff --git a/languages/ruby/bitwarden_sdk/bitwarden-sdk.gemspec b/languages/ruby/bitwarden_sdk/bitwarden-sdk.gemspec new file mode 100644 index 000000000..12a4f5fdc --- /dev/null +++ b/languages/ruby/bitwarden_sdk/bitwarden-sdk.gemspec @@ -0,0 +1,47 @@ +# frozen_string_literal: true + +require_relative 'lib/version' + +Gem::Specification.new do |spec| + spec.name = 'bitwarden-sdk' + spec.version = BitwardenSDK::VERSION + spec.authors = ['Bitwarden Inc.'] + spec.email = ['hello@bitwarden_sdk.com'] + + spec.summary = 'Bitwarden Secrets Manager SDK.' + spec.description = 'Ruby wrapper for Bitwarden secrets manager SDK.' + spec.homepage = 'https://bitwarden.com/products/secrets-manager/' + spec.required_ruby_version = '>= 3.0.0' + + spec.metadata['homepage_uri'] = spec.homepage + spec.metadata['source_code_uri'] = 'https://github.com/bitwarden/sdk' + spec.metadata['changelog_uri'] = 'https://github.com/bitwarden/sdk/blob/master/languages/ruby/CHANGELOG.md' + + # Specify which files should be added to the gem when it is released. + # The `git ls-files -z` loads the files in the RubyGem that have been added into git. + spec.files = Dir.chdir(__dir__) do + `git ls-files -z`.split("\x0").reject do |f| + (File.expand_path(f) == __FILE__) || f.start_with?(*%w[bin/ test/ spec/ features/ .git .circleci appveyor]) + end + end + + spec.files += Dir.glob('lib/ubuntu-x64/**/*') + spec.files += Dir.glob('lib/macos-x64/**/*') + spec.files += Dir.glob('lib/windows-x64/**/*') + spec.files += Dir.glob('lib/macos-arm64/**/*') + spec.files += Dir.glob('lib/schemas.rb') + + spec.bindir = 'exe' + spec.executables = spec.files.grep(%r{\Aexe/}) { |f| File.basename(f) } + spec.require_paths = ['lib'] + + # Uncomment to register a new dependency of your gem + # spec.add_dependency "example-gem", "~> 1.0" + spec.add_dependency 'dry-struct', '~> 1.6' + spec.add_dependency 'dry-types', '~> 1.7' + spec.add_dependency 'ffi', '~> 1.15' + spec.add_dependency 'json', '~> 2.6' + spec.add_dependency 'rake', '~> 13.0' + spec.add_dependency 'rubocop', '~> 1.21' + +end diff --git a/languages/ruby/bitwarden_sdk/lib/bitwarden-sdk.rb b/languages/ruby/bitwarden_sdk/lib/bitwarden-sdk.rb new file mode 100644 index 000000000..c2ce7e576 --- /dev/null +++ b/languages/ruby/bitwarden_sdk/lib/bitwarden-sdk.rb @@ -0,0 +1,56 @@ +# frozen_string_literal: true + +require 'json' +require 'dry-types' + +require_relative 'schemas' +require_relative 'extended_schemas/schemas' +require_relative 'command_runner' +require_relative 'bitwarden_lib' +require_relative 'bitwarden_error' +require_relative 'projects' +require_relative 'secrets' + +module BitwardenSDK + class BitwardenSettings + attr_accessor :api_url, :identity_url + + def initialize(api_url, identity_url) + # if api_url.nil? || identity_url.nil? + # raise ArgumentError, "api_url and identity_url cannot be nil" + # end + + @api_url = api_url + @identity_url = identity_url + end + end + + class BitwardenClient + attr_reader :bitwarden, :project_client, :secrets_client + + def initialize(bitwarden_settings) + client_settings = ClientSettings.new( + api_url: bitwarden_settings.api_url, + identity_url: bitwarden_settings.identity_url, + user_agent: 'Bitwarden RUBY-SDK', + device_type: nil + ) + + @bitwarden = BitwardenLib + @handle = @bitwarden.init(client_settings.to_json) + @command_runner = CommandRunner.new(@bitwarden, @handle) + @project_client = ProjectsClient.new(@command_runner) + @secrets_client = SecretsClient.new(@command_runner) + end + + def access_token_login(access_token) + access_token_request = AccessTokenLoginRequest.new(access_token: access_token) + @command_runner.run(SelectiveCommand.new(access_token_login: access_token_request)) + nil + end + + def free_mem + @bitwarden.free_mem(@handle) + end + end +end diff --git a/languages/ruby/bitwarden_sdk/lib/bitwarden_error.rb b/languages/ruby/bitwarden_sdk/lib/bitwarden_error.rb new file mode 100644 index 000000000..ec9adf932 --- /dev/null +++ b/languages/ruby/bitwarden_sdk/lib/bitwarden_error.rb @@ -0,0 +1,9 @@ +# frozen_string_literal: true + +module BitwardenSDK + class BitwardenError < StandardError + def initialize(message = 'Error getting response') + super(message) + end + end +end diff --git a/languages/ruby/bitwarden_sdk/lib/bitwarden_lib.rb b/languages/ruby/bitwarden_sdk/lib/bitwarden_lib.rb new file mode 100644 index 000000000..e09541788 --- /dev/null +++ b/languages/ruby/bitwarden_sdk/lib/bitwarden_lib.rb @@ -0,0 +1,35 @@ +# frozen_string_literal: true + +require 'ffi' + +module BitwardenSDK + module BitwardenLib + extend FFI::Library + + def self.mac_with_intel? + `uname -m`.strip == 'x86_64' + end + + ffi_lib case RUBY_PLATFORM + when /darwin/ + local_file = if mac_with_intel? + File.expand_path('macos-x64/libbitwarden_c.dylib', __dir__) + else + File.expand_path('macos-arm64/libbitwarden_c.dylib', __dir__) + end + File.exist?(local_file) ? local_file : File.expand_path('../../../../target/debug/libbitwarden_c.dylib', __dir__) + when /linux/ + local_file = File.expand_path('ubuntu-x64/libbitwarden_c.so', __dir__) + File.exist?(local_file) ? local_file : File.expand_path('../../../../target/debug/libbitwarden_c.so', __dir__) + when /mswin|mingw/ + local_file = File.expand_path('windows-x64/bitwarden_c.dll', __dir__) + File.exist?(local_file) ? local_file : File.expand_path('../../../../target/debug/bitwarden_c.dll', __dir__) + else + raise "Unsupported platform: #{RUBY_PLATFORM}" + end + + attach_function :init, [:string], :pointer + attach_function :run_command, %i[string pointer], :string + attach_function :free_mem, [:pointer], :void + end +end diff --git a/languages/ruby/bitwarden_sdk/lib/command_runner.rb b/languages/ruby/bitwarden_sdk/lib/command_runner.rb new file mode 100644 index 000000000..7da2269b6 --- /dev/null +++ b/languages/ruby/bitwarden_sdk/lib/command_runner.rb @@ -0,0 +1,15 @@ +# frozen_string_literal: true + +module BitwardenSDK + class CommandRunner + def initialize(bitwarden_sdk, handle) + @bitwarden_sdk = bitwarden_sdk + @handle = handle + end + + # @param [Dry-Struct] cmd + def run(cmd) + @bitwarden_sdk.run_command(cmd.to_json, @handle) + end + end +end diff --git a/languages/ruby/bitwarden_sdk/lib/extended_schemas/schemas.rb b/languages/ruby/bitwarden_sdk/lib/extended_schemas/schemas.rb new file mode 100644 index 000000000..be7ca2cbc --- /dev/null +++ b/languages/ruby/bitwarden_sdk/lib/extended_schemas/schemas.rb @@ -0,0 +1,64 @@ + +module BitwardenSDK + class SelectiveCommand < Command + attribute :password_login, PasswordLoginRequest.optional.default(nil) + attribute :api_key_login, APIKeyLoginRequest.optional.default(nil) + attribute :access_token_login, AccessTokenLoginRequest.optional.default(nil) + attribute :get_user_api_key, SecretVerificationRequest.optional.default(nil) + attribute :fingerprint, FingerprintRequest.optional.default(nil) + attribute :sync, SyncRequest.optional.default(nil) + attribute :secrets, SecretsCommand.optional.default(nil) + attribute :projects, ProjectsCommand.optional.default(nil) + + def to_dynamic + { + "passwordLogin" => password_login&.to_dynamic, + "apiKeyLogin" => api_key_login&.to_dynamic, + "accessTokenLogin" => access_token_login&.to_dynamic, + "getUserApiKey" => get_user_api_key&.to_dynamic, + "fingerprint" => fingerprint&.to_dynamic, + "sync" => sync&.to_dynamic, + "secrets" => secrets&.to_dynamic, + "projects" => projects&.to_dynamic, + }.compact + end + end + + class SelectiveProjectsCommand < ProjectsCommand + attribute :get, ProjectGetRequest.optional.default(nil) + attribute :create, ProjectCreateRequest.optional.default(nil) + attribute :list, ProjectsListRequest.optional.default(nil) + attribute :update, ProjectPutRequest.optional.default(nil) + attribute :delete, ProjectsDeleteRequest.optional.default(nil) + + def to_dynamic + { + "get" => get&.to_dynamic, + "create" => create&.to_dynamic, + "list" => list&.to_dynamic, + "update" => update&.to_dynamic, + "delete" => delete&.to_dynamic, + }.compact + end + end + + class SelectiveSecretsCommand < SecretsCommand + attribute :get, SecretGetRequest.optional.default(nil) + attribute :get_by_ids, SecretsGetRequest.optional.default(nil) + attribute :create, SecretCreateRequest.optional.default(nil) + attribute :list, SecretIdentifiersRequest.optional.default(nil) + attribute :update, SecretPutRequest.optional.default(nil) + attribute :delete, SecretsDeleteRequest.optional.default(nil) + + def to_dynamic + { + "get" => get&.to_dynamic, + "getByIds" => get_by_ids&.to_dynamic, + "create" => create&.to_dynamic, + "list" => list&.to_dynamic, + "update" => update&.to_dynamic, + "delete" => delete&.to_dynamic, + }.compact + end + end +end diff --git a/languages/ruby/bitwarden_sdk/lib/projects.rb b/languages/ruby/bitwarden_sdk/lib/projects.rb new file mode 100644 index 000000000..4363fcb9e --- /dev/null +++ b/languages/ruby/bitwarden_sdk/lib/projects.rb @@ -0,0 +1,116 @@ +# frozen_string_literal: true + +require_relative 'bitwarden_error' + +module BitwardenSDK + class ProjectsClient + def initialize(command_runner) + @command_runner = command_runner + end + + def create_project(project_name, organization_id) + project_create_request = ProjectCreateRequest.new( + project_create_request_name: project_name, + organization_id: organization_id + ) + command = create_command( + create: project_create_request + ) + response = parse_response(command) + + projects_response = ResponseForProjectResponse.from_json!(response).to_dynamic + + if projects_response.key?('success') && projects_response['success'] == true && + projects_response.key?('data') + return projects_response['data'] + end + + error_response(projects_response) + end + + def get(project_id) + project_get_request = ProjectGetRequest.new(id: project_id) + command = create_command(get: project_get_request) + response = parse_response(command) + + projects_response = ResponseForProjectResponse.from_json!(response).to_dynamic + + if projects_response.key?('success') && projects_response['success'] == true && + projects_response.key?('data') + return projects_response['data'] + end + + error_response(projects_response) + end + + def list_projects(organization_id) + project_list_request = ProjectsListRequest.new(organization_id: organization_id) + command = create_command(list: project_list_request) + response = parse_response(command) + + projects_response = ResponseForProjectsResponse.from_json!(response).to_dynamic + + if projects_response.key?('success') && projects_response['success'] == true && + projects_response.key?('data') && projects_response['data'].key?('data') + return projects_response['data']['data'] + end + + error_response(projects_response) + end + + def update_project(id, project_put_request_name, organization_id) + project_put_request = ProjectPutRequest.new( + id: id, + project_put_request_name: project_put_request_name, + organization_id: organization_id + ) + command = create_command( + update: project_put_request + ) + response = parse_response(command) + + projects_response = ResponseForProjectResponse.from_json!(response).to_dynamic + + if projects_response.key?('success') && projects_response['success'] == true && + projects_response.key?('data') + return projects_response['data'] + end + + error_response(projects_response) + end + + def delete_projects(ids) + project_delete_request = ProjectsDeleteRequest.new(ids: ids) + command = create_command(delete: project_delete_request) + response = parse_response(command) + + projects_response = ResponseForProjectsDeleteResponse.from_json!(response).to_dynamic + + if projects_response.key?('success') && projects_response['success'] == true && + projects_response.key?('data') && projects_response['data'].key?('data') + return projects_response['data']['data'] + end + + error_response(projects_response) + end + + private + + def error_response(response) + raise BitwardenError, response['errorMessage'] if response.key?('errorMessage') + + raise BitwardenError, 'Error while getting response' + end + + def create_command(commands) + SelectiveCommand.new(projects: SelectiveProjectsCommand.new(commands)) + end + + def parse_response(command) + response = @command_runner.run(command) + raise BitwardenError, 'Error getting response' if response.nil? + + response + end + end +end diff --git a/languages/ruby/bitwarden_sdk/lib/secrets.rb b/languages/ruby/bitwarden_sdk/lib/secrets.rb new file mode 100644 index 000000000..0eca98993 --- /dev/null +++ b/languages/ruby/bitwarden_sdk/lib/secrets.rb @@ -0,0 +1,124 @@ +# frozen_string_literal: true + +require 'json' + +module BitwardenSDK + class SecretsClient + def initialize(command_runner) + @command_runner = command_runner + end + + def get(id) + command = create_command(get: SecretGetRequest.new(id: id)) + response = run_command(command) + + secrets_response = ResponseForSecretResponse.from_json!(response).to_dynamic + + if secrets_response.key?('success') && secrets_response['success'] == true && + secrets_response.key?('data') + return secrets_response['data'] + end + + error_response(secrets_response) + end + + def get_by_ids(ids) + command = create_command(get_by_ids: SecretsGetRequest.new(ids: ids)) + response = run_command(command) + + secrets_response = ResponseForSecretIdentifiersResponse.from_json!(response).to_dynamic + + if secrets_response.key?('success') && secrets_response['success'] == true && + secrets_response.key?('data') && secrets_response['data'].key?('data') + return secrets_response['data']['data'] + end + + error_response(secrets_response) + end + + def create(key, note, organization_id, project_ids, value) + command = create_command( + create: SecretCreateRequest.new( + key: key, note: note, organization_id: organization_id, project_ids: project_ids, value: value + ) + ) + response = run_command(command) + + secrets_response = ResponseForSecretResponse.from_json!(response).to_dynamic + + if secrets_response.key?('success') && secrets_response['success'] == true && + secrets_response.key?('data') + return secrets_response['data'] + end + + error_response(secrets_response) + end + + def list(organization_id) + command = create_command(list: SecretIdentifiersRequest.new(organization_id: organization_id)) + response = run_command(command) + + secrets_response = ResponseForSecretIdentifiersResponse.from_json!(response).to_dynamic + + if secrets_response.key?('success') && secrets_response['success'] == true && + secrets_response.key?('data') && secrets_response['data'].key?('data') + return secrets_response['data']['data'] + end + + error_response(secrets_response) + end + + def update(id, key, note, organization_id, project_ids, value) + command = create_command( + update: SecretPutRequest.new( + id: id, key: key, note: note, organization_id: organization_id, project_ids: project_ids, value: value + ) + ) + response = run_command(command) + + secrets_response = ResponseForSecretResponse.from_json!(response).to_dynamic + + if secrets_response.key?('success') && secrets_response['success'] == true && + secrets_response.key?('data') + return secrets_response['data'] + end + + error_response(secrets_response) + end + + def delete_secret(ids) + command = create_command(delete: SecretsDeleteRequest.new(ids: ids)) + response = run_command(command) + + secrets_response = ResponseForSecretsDeleteResponse.from_json!(response).to_dynamic + + if secrets_response.key?('success') && secrets_response['success'] == true && + secrets_response.key?('data') && secrets_response['data'].key?('data') + return secrets_response['data']['data'] + end + + error_response(secrets_response) + end + + private + + def error_response(response) + if response['errorMessage'] + raise BitwardenError, response['errorMessage'] if response.key?('errorMessage') + else + raise BitwardenError, 'Error while getting response' + end + end + + def create_command(commands) + SelectiveCommand.new(secrets: SelectiveSecretsCommand.new(commands)) + end + + def run_command(command) + response = @command_runner.run(command) + raise BitwardenError, 'Error getting response' if response.nil? + + response + end + end +end diff --git a/languages/ruby/bitwarden_sdk/lib/version.rb b/languages/ruby/bitwarden_sdk/lib/version.rb new file mode 100644 index 000000000..1fd8c02c1 --- /dev/null +++ b/languages/ruby/bitwarden_sdk/lib/version.rb @@ -0,0 +1,5 @@ +# frozen_string_literal: true + +module BitwardenSDK + VERSION = '0.0.0' +end diff --git a/languages/ruby/bitwarden_sdk/sig/bitwarden-sdk.rbs b/languages/ruby/bitwarden_sdk/sig/bitwarden-sdk.rbs new file mode 100644 index 000000000..3f6a73f6a --- /dev/null +++ b/languages/ruby/bitwarden_sdk/sig/bitwarden-sdk.rbs @@ -0,0 +1,13 @@ +require_relative '../lib/schemas' + +class BitwardenClient + @command_runner: CommandRunner + + attr_reader bitwarden: Module + attr_reader project_client: ProjectsClient + attr_reader secrets_client: SecretsClient + + def initialize: (BitwardenSettings) -> void + def access_token_login: (String) -> JSON + def free_mem: () -> nil +end diff --git a/languages/ruby/bitwarden_sdk/sig/bitwarden_settings.rbs b/languages/ruby/bitwarden_sdk/sig/bitwarden_settings.rbs new file mode 100644 index 000000000..154ee16e5 --- /dev/null +++ b/languages/ruby/bitwarden_sdk/sig/bitwarden_settings.rbs @@ -0,0 +1,8 @@ +require_relative '../lib/schemas' + +class BitwardenSettings + attr_accessor api_url: String + attr_accessor identity_url: String + + def initialize: (String, String) -> void +end diff --git a/languages/ruby/bitwarden_sdk/sig/command_runner.rbs b/languages/ruby/bitwarden_sdk/sig/command_runner.rbs new file mode 100644 index 000000000..7a7a17dd9 --- /dev/null +++ b/languages/ruby/bitwarden_sdk/sig/command_runner.rbs @@ -0,0 +1,4 @@ +class CommandRunner + @bitwarden_sdk: Module + def run: -> String +end diff --git a/languages/ruby/bitwarden_sdk/sig/projects_client.rbs b/languages/ruby/bitwarden_sdk/sig/projects_client.rbs new file mode 100644 index 000000000..00c9e578d --- /dev/null +++ b/languages/ruby/bitwarden_sdk/sig/projects_client.rbs @@ -0,0 +1,17 @@ +require_once '../lib/extended_schemas/schemas.rbs' +require_once '../schemas.rbs' + +class ProjectsClient + @command_runner: CommandRunner + def initialize: (command_runner: CommandRunner) -> void + def create_project: (project_name: String, organization_id: String) -> ProjectsResponse + def get: (project_id: String) -> ProjectsResponse + def list_projects: (organization_id: String) -> Array(DatumElement) + def update_project: (id: String, project_put_request_name: String, organization_id: String) -> ProjectsResponse + def delete_projects: (ids: Array[String]) -> Array(ProjectDeleteResponse) + + private + + def create_command: (SelectiveProjectsCommand) -> SelectiveCommand + def parse_response: (ResponseForProjectResponse) -> ResponseForProjectResponse +end diff --git a/languages/ruby/bitwarden_sdk/sig/sdk.rbs b/languages/ruby/bitwarden_sdk/sig/sdk.rbs new file mode 100644 index 000000000..260fa1420 --- /dev/null +++ b/languages/ruby/bitwarden_sdk/sig/sdk.rbs @@ -0,0 +1,3 @@ +module BitwardenSDK + VERSION: String +end diff --git a/languages/ruby/bitwarden_sdk/sig/secrets_client.rbs b/languages/ruby/bitwarden_sdk/sig/secrets_client.rbs new file mode 100644 index 000000000..ccebcecd8 --- /dev/null +++ b/languages/ruby/bitwarden_sdk/sig/secrets_client.rbs @@ -0,0 +1,18 @@ +require_once '../lib/extended_schemas/schemas.rbs' +require_once '../schemas.rbs' + +class SecretsClient + # @command_runner: CommandRunner + def initialize: (command_runner: CommandRunner) -> void + def get: (id: String) -> SecretResponse + def get_by_ids: (ids: Array[String]) -> Array(SecretIdentifierResponse) + def create: (key: String, note: String, organization_id: String, project_ids: Array[String], value: String) -> SecretResponse + def list: (organization_id: String) -> Array(SecretIdentifierResponse) + def update: (id: String, key: String, note: String, organization_id: String, project_ids: Array[String], value: String) -> SecretResponse + def delete_secret: (ids: Array[String]) -> Array(SecretDeleteResponse) + + private + + def create_command: (SelectiveSecretsCommand) -> SelectiveCommand + def parse_response: (SelectiveSecretCommand) -> ResponseForSecretResponse +end diff --git a/languages/ruby/examples/example.rb b/languages/ruby/examples/example.rb new file mode 100644 index 000000000..c0deeb437 --- /dev/null +++ b/languages/ruby/examples/example.rb @@ -0,0 +1,67 @@ +# NOTE - for example purpose only - import gem instead +require 'bitwarden-sdk' + +token = '' +organization_id = '' + +bitwarden_settings = BitwardenSDK::BitwardenSettings.new( + 'https://api.bitwarden.com', + 'https://identity.bitwarden.com/connect/token' +) + +bw_client = BitwardenSDK::BitwardenClient.new(bitwarden_settings) +response = bw_client.access_token_login(token) +puts response + +# CREATE project +project_name = 'Test project 1' +response = bw_client.project_client.create_project(project_name, organization_id) +puts response +project_id = response['id'] + +# GET project +response = bw_client.project_client.get(project_id) +puts response + +# LIST projects +response = bw_client.project_client.list_projects(organization_id) +puts response + +# UPDATE projects +name = 'Updated test project 1' +response = bw_client.project_client.update_project(project_id, name, organization_id) +puts response + +# CREATE secret +key = 'AWS-SES' +note = 'Private account' +value = '8t27.dfj;' +response = bw_client.secrets_client.create(key, note, organization_id, [project_id], value) +puts response +secret_id = response['id'] + +# GET secret +response = bw_client.secrets_client.get(secret_id) +puts response + +# GET secret by ids +response = bw_client.secrets_client.get_by_ids([secret_id]) +puts response + +# LIST secrets +response = bw_client.secrets_client.list(organization_id) +puts response + +# UPDATE secret +note = 'updated password' +value = '7I.ert10AjK' +response = bw_client.secrets_client.update(secret_id, key, note,organization_id, [project_id], value) +puts response + +# DELETE secret +response = bw_client.secrets_client.delete_secret([secret_id]) +puts response + +# DELETE project +response = bw_client.project_client.delete_projects([project_id]) +puts response diff --git a/languages/swift/iOS/App.xcodeproj/project.pbxproj b/languages/swift/iOS/App.xcodeproj/project.pbxproj index 003c09c0d..e8b3519fd 100644 --- a/languages/swift/iOS/App.xcodeproj/project.pbxproj +++ b/languages/swift/iOS/App.xcodeproj/project.pbxproj @@ -7,6 +7,7 @@ objects = { /* Begin PBXBuildFile section */ + 33F2B0502B0511C700E1E91C /* Biometrics.swift in Sources */ = {isa = PBXBuildFile; fileRef = 33F2B04F2B0511C700E1E91C /* Biometrics.swift */; }; 55B6153E2A8678B300BE93F4 /* testApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = 55B6153D2A8678B300BE93F4 /* testApp.swift */; }; 55B615402A8678B300BE93F4 /* ContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 55B6153F2A8678B300BE93F4 /* ContentView.swift */; }; 55B615422A8678B400BE93F4 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 55B615412A8678B400BE93F4 /* Assets.xcassets */; }; @@ -15,6 +16,7 @@ /* End PBXBuildFile section */ /* Begin PBXFileReference section */ + 33F2B04F2B0511C700E1E91C /* Biometrics.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Biometrics.swift; sourceTree = ""; }; 55B6153A2A8678B300BE93F4 /* App.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = App.app; sourceTree = BUILT_PRODUCTS_DIR; }; 55B6153D2A8678B300BE93F4 /* testApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = testApp.swift; sourceTree = ""; }; 55B6153F2A8678B300BE93F4 /* ContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentView.swift; sourceTree = ""; }; @@ -60,6 +62,7 @@ 55B6153F2A8678B300BE93F4 /* ContentView.swift */, 55B615412A8678B400BE93F4 /* Assets.xcassets */, 55B615432A8678B400BE93F4 /* Preview Content */, + 33F2B04F2B0511C700E1E91C /* Biometrics.swift */, ); path = App; sourceTree = ""; @@ -118,7 +121,7 @@ attributes = { BuildIndependentTargetsInParallel = 1; LastSwiftUpdateCheck = 1420; - LastUpgradeCheck = 1420; + LastUpgradeCheck = 1500; TargetAttributes = { 55B615392A8678B300BE93F4 = { CreatedOnToolsVersion = 14.2; @@ -160,6 +163,7 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + 33F2B0502B0511C700E1E91C /* Biometrics.swift in Sources */, 55B615402A8678B300BE93F4 /* ContentView.swift in Sources */, 55B6153E2A8678B300BE93F4 /* testApp.swift in Sources */, ); @@ -172,6 +176,7 @@ isa = XCBuildConfiguration; buildSettings = { ALWAYS_SEARCH_USER_PATHS = NO; + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; CLANG_ANALYZER_NONNULL = YES; CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; @@ -204,6 +209,7 @@ DEBUG_INFORMATION_FORMAT = dwarf; ENABLE_STRICT_OBJC_MSGSEND = YES; ENABLE_TESTABILITY = YES; + ENABLE_USER_SCRIPT_SANDBOXING = YES; GCC_C_LANGUAGE_STANDARD = gnu11; GCC_DYNAMIC_NO_PIC = NO; GCC_NO_COMMON_BLOCKS = YES; @@ -232,6 +238,7 @@ isa = XCBuildConfiguration; buildSettings = { ALWAYS_SEARCH_USER_PATHS = NO; + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; CLANG_ANALYZER_NONNULL = YES; CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; @@ -264,6 +271,7 @@ DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; ENABLE_NS_ASSERTIONS = NO; ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_USER_SCRIPT_SANDBOXING = YES; GCC_C_LANGUAGE_STANDARD = gnu11; GCC_NO_COMMON_BLOCKS = YES; GCC_WARN_64_TO_32_BIT_CONVERSION = YES; @@ -293,6 +301,7 @@ DEVELOPMENT_TEAM = LTZ2PFU5D6; ENABLE_PREVIEWS = YES; GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_KEY_NSFaceIDUsageDescription = "Unlock vault with biometrics"; INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES; INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; INFOPLIST_KEY_UILaunchScreen_Generation = YES; @@ -323,6 +332,7 @@ DEVELOPMENT_TEAM = LTZ2PFU5D6; ENABLE_PREVIEWS = YES; GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_KEY_NSFaceIDUsageDescription = "Unlock vault with biometrics"; INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES; INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; INFOPLIST_KEY_UILaunchScreen_Generation = YES; diff --git a/languages/swift/iOS/App/Biometrics.swift b/languages/swift/iOS/App/Biometrics.swift new file mode 100644 index 000000000..2fc5ef2a3 --- /dev/null +++ b/languages/swift/iOS/App/Biometrics.swift @@ -0,0 +1,92 @@ +// +// Biometrics.swift +// App +// +// Created by Dani on 15/11/23. +// + +/** + * IMPORTANT: This file is provided only for the purpose of demostrating the use of the biometric unlock functionality. + * It hasn't gone through a throrough security review and should not be considered production ready. It also doesn't + * handle a lot of errors and edge cases that a production application would need to deal with. + * Developers are encouraged to review and improve the code as needed to meet their security requirements. + * Additionally, we recommend to consult with security experts and conduct thorough testing before using the code in production. + */ + +import Foundation +import LocalAuthentication + +let SERVICE: String = "com.example.app" + +// We should separate keys for each user by appending the user_id +let KEY: String = "biometric_key" + + +func biometricStoreValue(value: String) { + var error: Unmanaged? + let accessControl = SecAccessControlCreateWithFlags( + nil, + kSecAttrAccessibleWhenUnlockedThisDeviceOnly, + .biometryCurrentSet, + &error) + + guard accessControl != nil && error == nil else { + fatalError("SecAccessControlCreateWithFlags failed") + } + + let query = [ + kSecClass: kSecClassGenericPassword, + kSecAttrService: SERVICE, + kSecAttrAccount: KEY, + kSecValueData: value.data(using: .utf8)!, + kSecAttrAccessControl: accessControl as Any + ] as CFDictionary + + // Try to delete the previous secret, if it exists + // Otherwise we get `errSecDuplicateItem` + SecItemDelete(query) + + let status = SecItemAdd(query, nil) + guard status == errSecSuccess else { + fatalError("Unable to store the secret: " + errToString(status: status)) + } +} + +private func errToString(status: OSStatus) -> String { + if let err = SecCopyErrorMessageString(status, nil) as String? { + err + } else { + "Unknown error" + } +} + +func biometricRetrieveValue() -> String? { + let searchQuery = [ + kSecClass: kSecClassGenericPassword, + kSecAttrService: SERVICE, + kSecAttrAccount: KEY, + kSecMatchLimit: kSecMatchLimitOne, + kSecReturnData: true, + kSecReturnAttributes: true, + ] as CFDictionary + + var item: AnyObject? + let status = SecItemCopyMatching(searchQuery, &item) + + // If the item is not found, we just return nil + if status == errSecItemNotFound { + return nil + } + + // TODO: We probably want to handle these errors better + guard status == noErr else { + fatalError("Unable to retrieve the secret: " + errToString(status: status)) + } + + if let resultDictionary = item as? [String: Any], + let data = resultDictionary[kSecValueData as String] as? Data { + return String(decoding: data, as: UTF8.self) + } + + return nil +} diff --git a/languages/swift/iOS/App/ContentView.swift b/languages/swift/iOS/App/ContentView.swift index 4b3555403..5df7dc3fd 100644 --- a/languages/swift/iOS/App/ContentView.swift +++ b/languages/swift/iOS/App/ContentView.swift @@ -8,170 +8,261 @@ import BitwardenSdk import SwiftUI -struct ContentView: View { - - @State private var msg: String - - init() { - let client = Client(settings: nil) - - _msg = State(initialValue: client.echo(msg: "Sdk")) +/** + * IMPORTANT: This file is provided only for the purpose of demostrating the use of the SDK functionality. + * It hasn't gone through a throrough security review and should not be considered production ready. It also doesn't + * handle a lot of errors and edge cases that a production application would need to deal with. + * Developers are encouraged to review and improve the code as needed to meet their security requirements. + * Additionally, we recommend to consult with security experts and conduct thorough testing before using the code in production. + */ - let SERVER_URL = "https://localhost:8080/" - let API_URL = SERVER_URL + "api/" - let IDENTITY_URL = SERVER_URL + "identity/" +let SERVER_URL = "https://localhost:8080/" +let API_URL = SERVER_URL + "api/" +let IDENTITY_URL = SERVER_URL + "identity/" - let EMAIL = "test@bitwarden.com" - let PASSWORD = "asdfasdfasdf" +let EMAIL = "test@bitwarden.com" +let PASSWORD = "asdfasdfasdf" +struct ContentView: View { + private var http: URLSession + + @State private var client: Client + + @State private var accessToken: String = "" + + init() { // Disable SSL Cert validation. Don't do this in production - let ignoreHttpsDelegate = IgnoreHttpsDelegate() - let http = URLSession( - configuration: URLSessionConfiguration.default, delegate: ignoreHttpsDelegate, + http = URLSession( + configuration: URLSessionConfiguration.default, delegate: IgnoreHttpsDelegate(), delegateQueue: nil) - - Task { - - ////////////////////////////// Get master password hash ////////////////////////////// - - struct PreloginRequest: Codable { let email: String } - struct PreloginResponse: Codable { - let kdf: UInt32 - let kdfIterations: UInt32 - let kdfMemory: UInt32? - let kdfParallelism: UInt32? - - } - - let (preloginDataJson, _) = try await http.data( - for: request( - method: "POST", url: IDENTITY_URL + "accounts/prelogin", - fn: { r in - r.setValue("application/json", forHTTPHeaderField: "Content-Type") - r.httpBody = try JSONEncoder().encode(PreloginRequest(email: EMAIL)) - })) - let preloginData = try JSONDecoder().decode( - PreloginResponse.self, from: preloginDataJson) - - let kdf: Kdf - if preloginData.kdf == 0 { - kdf = Kdf.pbkdf2(iterations: preloginData.kdfIterations) - } else { - kdf = Kdf.argon2id( - iterations: preloginData.kdfIterations, memory: preloginData.kdfMemory!, - parallelism: preloginData.kdfParallelism!) - } - - let passwordHash = try await client.auth().hashPassword( - email: EMAIL, password: PASSWORD, kdfParams: kdf) - - ///////////////////////////// Login ///////////////////////////// - - struct LoginResponse: Codable { - let Key: String - let PrivateKey: String - let access_token: String - let refresh_token: String - } - - let (loginDataJson, _) = try await http.data( - for: request( - method: "POST", url: IDENTITY_URL + "connect/token", - fn: { r in - r.setValue( - EMAIL.data(using: .utf8)?.base64EncodedString(), - forHTTPHeaderField: "Auth-Email") - r.setValue( - "application/x-www-form-urlencoded", forHTTPHeaderField: "Content-Type") - - var comp = URLComponents() - comp.queryItems = [ - URLQueryItem(name: "scope", value: "api offline_access"), - URLQueryItem(name: "client_id", value: "web"), - URLQueryItem(name: "deviceType", value: "12"), - URLQueryItem( - name: "deviceIdentifier", - value: "0745d426-8dab-484a-9816-4959721d77c7"), - URLQueryItem(name: "deviceName", value: "edge"), - URLQueryItem(name: "grant_type", value: "password"), - URLQueryItem(name: "username", value: EMAIL), - URLQueryItem(name: "password", value: passwordHash), - ] - r.httpBody = comp.percentEncodedQuery! - .replacingOccurrences(of: "@", with: "%40") - .replacingOccurrences(of: "+", with: "%2B") - .data(using: .utf8) - })) - let loginData = try JSONDecoder().decode(LoginResponse.self, from: loginDataJson) - - ///////////////////////////// Sync ///////////////////////////// - - struct SyncOrganization: Codable { - let id: String - let key: String - } - struct SyncProfile: Codable { - let organizations: [SyncOrganization] - - } - struct SyncFolder: Codable { - let id: String - let name: String - let revisionDate: String - } - struct SyncResponse: Codable { - let profile: SyncProfile - let folders: [SyncFolder] - } - - let (syncDataJson, _) = try await http.data( - for: request( - method: "GET", url: API_URL + "sync?excludeDomains=true", - fn: { r in - r.setValue( - "Bearer " + loginData.access_token, forHTTPHeaderField: "Authorization") - })) - - let syncData = try JSONDecoder().decode(SyncResponse.self, from: syncDataJson) - - ///////////////////////////// Initialize crypto ///////////////////////////// - - try await client.crypto().initializeCrypto( - req: InitCryptoRequest( - kdfParams: kdf, - email: EMAIL, - password: PASSWORD, - userKey: loginData.Key, - privateKey: loginData.PrivateKey, - organizationKeys: Dictionary.init( - uniqueKeysWithValues: syncData.profile.organizations.map { ($0.id, $0.key) } - ) - )) - - ///////////////////////////// Decrypt some folders ///////////////////////////// - - let dateFormatter = ISO8601DateFormatter() - dateFormatter.formatOptions = [.withFractionalSeconds] - - let decryptedFolders = try await client.vault().folders().decryptList( - folders: syncData.folders.map { - Folder( - id: $0.id, name: $0.name, - revisionDate: dateFormatter.date(from: $0.revisionDate)!) - }) - print(decryptedFolders) - } + + client = Client(settings: nil) } - + + @State var setupBiometrics: Bool = true + @State var outputText: String = "" + var body: some View { VStack { - Image(systemName: "globe") - .imageScale(.large) - .foregroundColor(.accentColor) - Text("Hello " + msg) + Toggle("Setup biometric unlock after login", isOn: $setupBiometrics).padding(.init(top: 0, leading: 20, bottom: 0, trailing: 20)) + + Button(action: { + Task { + do { + try await clientExamplePassword(clientAuth: client.auth(), clientCrypto: client.crypto(), setupBiometrics: setupBiometrics) + try await decryptVault(clientCrypto: client.crypto(), clientVault: client.vault()) + } catch { + print("ERROR:", error) + } + } + }, label: { + Text("Login with username + password") + }) + + Divider().padding(30) + + Button(action: { + Task { + do { + try await clientExampleBiometrics(clientCrypto: client.crypto()) + try await decryptVault(clientCrypto: client.crypto(), clientVault: client.vault()) + } catch { + print("ERROR:", error) + } + } + }, label: { + Text("Unlock with biometrics") + }) + + Button(action: { + client = Client(settings: nil) + }, label: { + Text("Lock & reset client") + }).padding() + + Text("Output: " + outputText).padding(.top) } .padding() } + + func clientExamplePassword(clientAuth: ClientAuthProtocol, clientCrypto: ClientCryptoProtocol, setupBiometrics: Bool) async throws { + ////////////////////////////// Get master password hash ////////////////////////////// + + struct PreloginRequest: Codable { let email: String } + struct PreloginResponse: Codable { + let kdf: UInt32 + let kdfIterations: UInt32 + let kdfMemory: UInt32? + let kdfParallelism: UInt32? + + } + + let (preloginDataJson, _) = try await http.data( + for: request( + method: "POST", url: IDENTITY_URL + "accounts/prelogin", + fn: { r in + r.setValue("application/json", forHTTPHeaderField: "Content-Type") + r.httpBody = try JSONEncoder().encode(PreloginRequest(email: EMAIL)) + })) + let preloginData = try JSONDecoder().decode( + PreloginResponse.self, from: preloginDataJson) + + let kdf: Kdf + if preloginData.kdf == 0 { + kdf = Kdf.pbkdf2(iterations: preloginData.kdfIterations) + } else { + kdf = Kdf.argon2id( + iterations: preloginData.kdfIterations, memory: preloginData.kdfMemory!, + parallelism: preloginData.kdfParallelism!) + } + + let passwordHash = try await clientAuth.hashPassword( + email: EMAIL, password: PASSWORD, kdfParams: kdf) + + ///////////////////////////// Login ///////////////////////////// + + struct LoginResponse: Codable { + let Key: String + let PrivateKey: String + let access_token: String + let refresh_token: String + } + + let (loginDataJson, _) = try await http.data( + for: request( + method: "POST", url: IDENTITY_URL + "connect/token", + fn: { r in + r.setValue( + EMAIL.data(using: .utf8)?.base64EncodedString(), + forHTTPHeaderField: "Auth-Email") + r.setValue( + "application/x-www-form-urlencoded", forHTTPHeaderField: "Content-Type") + + var comp = URLComponents() + comp.queryItems = [ + URLQueryItem(name: "scope", value: "api offline_access"), + URLQueryItem(name: "client_id", value: "web"), + URLQueryItem(name: "deviceType", value: "12"), + URLQueryItem( + name: "deviceIdentifier", + value: "0745d426-8dab-484a-9816-4959721d77c7"), + URLQueryItem(name: "deviceName", value: "edge"), + URLQueryItem(name: "grant_type", value: "password"), + URLQueryItem(name: "username", value: EMAIL), + URLQueryItem(name: "password", value: passwordHash), + ] + r.httpBody = comp.percentEncodedQuery! + .replacingOccurrences(of: "@", with: "%40") + .replacingOccurrences(of: "+", with: "%2B") + .data(using: .utf8) + })) + let loginData = try JSONDecoder().decode(LoginResponse.self, from: loginDataJson) + + try await clientCrypto.initializeUserCrypto( + req: InitUserCryptoRequest( + kdfParams: kdf, + email: EMAIL, + privateKey: loginData.PrivateKey, + method: InitUserCryptoMethod.password( + password: PASSWORD, + userKey: loginData.Key + ) + )) + + accessToken = loginData.access_token + + if (setupBiometrics) { + let defaults = UserDefaults.standard + defaults.set(loginData.PrivateKey, forKey: "privateKey") + defaults.set(preloginData.kdf, forKey: "kdfType") + defaults.set(preloginData.kdfIterations, forKey: "kdfIterations") + defaults.set(preloginData.kdfMemory, forKey: "kdfMemory") + defaults.set(preloginData.kdfParallelism, forKey: "kdfParallelism") + defaults.synchronize() + + let key = try await clientCrypto.getUserEncryptionKey() + biometricStoreValue(value: key) + } + } + + func clientExampleBiometrics(clientCrypto: ClientCryptoProtocol) async throws { + let defaults = UserDefaults.standard + let privateKey = defaults.string(forKey: "privateKey")! + let kdf = if defaults.integer(forKey: "kdfType") == 0 { + Kdf.pbkdf2(iterations: UInt32(defaults.integer(forKey: "kdfIterations"))) + } else { + Kdf.argon2id( + iterations: UInt32(defaults.integer(forKey: "kdfIterations")), + memory: UInt32(defaults.integer(forKey: "kdfMemory")), + parallelism: UInt32(defaults.integer(forKey: "kdfParallelism")) + ) + } + + let key = biometricRetrieveValue()! + + try await clientCrypto.initializeUserCrypto(req: InitUserCryptoRequest( + kdfParams: kdf, + email: EMAIL, + privateKey: privateKey, + method: InitUserCryptoMethod.decryptedKey( + decryptedUserKey: key + ) + )) + } + + func decryptVault(clientCrypto: ClientCryptoProtocol, clientVault: ClientVaultProtocol) async throws { + ///////////////////////////// Sync ///////////////////////////// + + struct SyncOrganization: Codable { + let id: String + let key: String + } + struct SyncProfile: Codable { + let organizations: [SyncOrganization] + + } + struct SyncFolder: Codable { + let id: String + let name: String + let revisionDate: String + } + struct SyncResponse: Codable { + let profile: SyncProfile + let folders: [SyncFolder] + } + + let (syncDataJson, _) = try await http.data( + for: request( + method: "GET", url: API_URL + "sync?excludeDomains=true", + fn: { r in + r.setValue( + "Bearer " + accessToken, forHTTPHeaderField: "Authorization") + })) + + let syncData = try JSONDecoder().decode(SyncResponse.self, from: syncDataJson) + + ///////////////////////////// Initialize org crypto ///////////////////////////// + + try await clientCrypto.initializeOrgCrypto( + req: InitOrgCryptoRequest( + organizationKeys: Dictionary.init( + uniqueKeysWithValues: syncData.profile.organizations.map { ($0.id, $0.key) } + ) + )) + + ///////////////////////////// Decrypt some folders ///////////////////////////// + + let dateFormatter = ISO8601DateFormatter() + dateFormatter.formatOptions = [.withFractionalSeconds] + + let decryptedFolders = try await clientVault.folders().decryptList( + folders: syncData.folders.map { + Folder( + id: $0.id, name: $0.name, + revisionDate: dateFormatter.date(from: $0.revisionDate)!) + }) + print(decryptedFolders) + } } struct ContentView_Previews: PreviewProvider { @@ -196,7 +287,7 @@ extension IgnoreHttpsDelegate: URLSessionDelegate { ) { //Trust the certificate even if not valid let urlCredential = URLCredential(trust: challenge.protectionSpace.serverTrust!) - + completionHandler(.useCredential, urlCredential) } } diff --git a/package-lock.json b/package-lock.json index 2ee17dd0a..6ee8982ec 100644 --- a/package-lock.json +++ b/package-lock.json @@ -11,8 +11,8 @@ "devDependencies": { "@openapitools/openapi-generator-cli": "2.7.0", "handlebars": "^4.7.8", - "prettier": "3.0.3", - "quicktype-core": "23.0.76", + "prettier": "3.1.0", + "quicktype-core": "23.0.79", "rimraf": "5.0.5", "ts-node": "10.9.1", "typescript": "5.2.2" @@ -363,9 +363,9 @@ "dev": true }, "node_modules/@types/node": { - "version": "20.8.9", - "resolved": "https://registry.npmjs.org/@types/node/-/node-20.8.9.tgz", - "integrity": "sha512-UzykFsT3FhHb1h7yD4CA4YhBHq545JC0YnEz41xkipN88eKQtL6rSgocL5tbAP6Ola9Izm/Aw4Ora8He4x0BHg==", + "version": "20.9.2", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.9.2.tgz", + "integrity": "sha512-WHZXKFCEyIUJzAwh3NyyTHYSR35SevJ6mZ1nWwJafKtiQbqRTIKSRcw3Ma3acqgsent3RRDqeVwpHntMk+9irg==", "dev": true, "peer": true, "dependencies": { @@ -373,9 +373,9 @@ } }, "node_modules/@types/urijs": { - "version": "1.19.22", - "resolved": "https://registry.npmjs.org/@types/urijs/-/urijs-1.19.22.tgz", - "integrity": "sha512-qnYBwfN7O/+i6E1Kr8JaCKsrCLpRCiQ1XxkSxNIYuJ/5Aagt0+HlMX78DJMUrNzDULMz0eu2gcprlxJaDtACOw==", + "version": "1.19.23", + "resolved": "https://registry.npmjs.org/@types/urijs/-/urijs-1.19.23.tgz", + "integrity": "sha512-3Zbk6RzmIpvKTNEHO2RcPOGHM++BQEITMqBRR1Ju32WbruhV/pygYgxiP3xA0b1B88zjzs0Izzjxsbj768+IjA==", "dev": true }, "node_modules/abort-controller": { @@ -1252,9 +1252,9 @@ } }, "node_modules/lru-cache": { - "version": "10.0.1", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.0.1.tgz", - "integrity": "sha512-IJ4uwUTi2qCccrioU6g9g/5rvvVl13bsdczUUcqbciD9iLr095yj8DQKdObriEvuNSx325N1rV1O0sJFszx75g==", + "version": "10.0.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.0.3.tgz", + "integrity": "sha512-B7gr+F6MkqB3uzINHXNctGieGsRTMwIBgxkp0yq/5BwcuDzD4A8wQpHQW6vDAm1uKSLQghmRdD9sKqf2vJ1cEg==", "dev": true, "engines": { "node": "14 || >=16.14" @@ -1470,9 +1470,9 @@ } }, "node_modules/prettier": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.0.3.tgz", - "integrity": "sha512-L/4pUDMxcNa8R/EthV08Zt42WBO4h1rarVtK0K+QJG0X187OLo7l699jWw0GKuwzkPQ//jMFA/8Xm6Fh3J/DAg==", + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.1.0.tgz", + "integrity": "sha512-TQLvXjq5IAibjh8EpBIkNKxO749UEWABoiIZehEPiY4GNpVdhaFKqSTu+QrlU6D2dPAfubRmtJTi4K4YkQ5eXw==", "dev": true, "bin": { "prettier": "bin/prettier.cjs" @@ -1494,9 +1494,9 @@ } }, "node_modules/quicktype-core": { - "version": "23.0.76", - "resolved": "https://registry.npmjs.org/quicktype-core/-/quicktype-core-23.0.76.tgz", - "integrity": "sha512-QinZRNovSTQcFuhRKxeHb22eFmyucbG96EPaQDSbz9qvIPxUhs1BZviNc8HAkHWYFqTSET/xZcEoHpm1DeDbRg==", + "version": "23.0.79", + "resolved": "https://registry.npmjs.org/quicktype-core/-/quicktype-core-23.0.79.tgz", + "integrity": "sha512-Auzy8AhorFt6YGeB53/dzUSINmKKassAyCsr2wpNgG9XoC3i6oUoLuySNUzYIkyCFCGmKdBRBQeyAqPOVteoYw==", "dev": true, "dependencies": { "@glideapps/ts-necessities": "2.1.3", @@ -2007,9 +2007,9 @@ "dev": true }, "node_modules/universalify": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.0.tgz", - "integrity": "sha512-hAZsKq7Yy11Zu1DE0OzWjw7nnLZmJZYTDZZyEFHZdUhV8FkH5MCfoU1XMaxXovpyW5nq5scPqq0ZDP9Zyl04oQ==", + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", + "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", "dev": true, "engines": { "node": ">= 10.0.0" @@ -2130,9 +2130,9 @@ } }, "node_modules/yaml": { - "version": "2.3.3", - "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.3.3.tgz", - "integrity": "sha512-zw0VAJxgeZ6+++/su5AFoqBbZbrEakwu+X0M5HmcwUiBL7AzcuPKjj5we4xfQLp78LkEMpD0cOnUhmgOVy3KdQ==", + "version": "2.3.4", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.3.4.tgz", + "integrity": "sha512-8aAvwVUSHpfEqTQ4w/KMlf3HcRdt50E5ODIQJBw1fQ5RL34xabzxtUlzTXVqc4rkZsPbvrXKWnABCD7kWSmocA==", "dev": true, "engines": { "node": ">= 14" diff --git a/package.json b/package.json index fdeb8fe58..97f08c571 100644 --- a/package.json +++ b/package.json @@ -22,8 +22,8 @@ "devDependencies": { "@openapitools/openapi-generator-cli": "2.7.0", "handlebars": "^4.7.8", - "prettier": "3.0.3", - "quicktype-core": "23.0.76", + "prettier": "3.1.0", + "quicktype-core": "23.0.79", "rimraf": "5.0.5", "ts-node": "10.9.1", "typescript": "5.2.2" diff --git a/sig/bitwarden_sdk/bitwarden_client.rbs b/sig/bitwarden_sdk/bitwarden_client.rbs new file mode 100644 index 000000000..1ff97eb92 --- /dev/null +++ b/sig/bitwarden_sdk/bitwarden_client.rbs @@ -0,0 +1,5 @@ +module BitwardenSDK + class BitwardenClient + attr_reader project_client: ProjectsClient + end +end diff --git a/support/docs/docs.ts b/support/docs/docs.ts index 14603dd57..b547b4502 100644 --- a/support/docs/docs.ts +++ b/support/docs/docs.ts @@ -28,6 +28,7 @@ const rootElements = [ "ClientFolders", "ClientGenerators", "ClientPasswordHistory", + "ClientPlatform", "ClientSends", "ClientVault", ]; diff --git a/support/scripts/schemas.ts b/support/scripts/schemas.ts index 148b089e6..9e25bedc5 100644 --- a/support/scripts/schemas.ts +++ b/support/scripts/schemas.ts @@ -1,4 +1,10 @@ -import { quicktype, InputData, JSONSchemaInput, FetchingJSONSchemaStore } from "quicktype-core"; +import { + quicktype, + quicktypeMultiFile, + InputData, + JSONSchemaInput, + FetchingJSONSchemaStore, +} from "quicktype-core"; import fs from "fs"; import path from "path"; @@ -52,6 +58,16 @@ async function main() { writeToFile("./languages/python/BitwardenClient/schemas.py", python.lines); + const ruby = await quicktype({ + inputData, + lang: "ruby", + rendererOptions: { + "ruby-version": "3.0", + }, + }); + + writeToFile("./languages/ruby/bitwarden_sdk/lib/schemas.rb", ruby.lines); + const csharp = await quicktype({ inputData, lang: "csharp", @@ -63,6 +79,50 @@ async function main() { }); writeToFile("./languages/csharp/Bitwarden.Sdk/schemas.cs", csharp.lines); + + const cpp = await quicktype({ + inputData, + lang: "cpp", + rendererOptions: { + namespace: "Bitwarden::Sdk", + "include-location": "global-include", + }, + }); + + cpp.lines.forEach((line, idx) => { + // Replace DOMAIN for URI_DOMAIN, because DOMAIN is an already defined macro + cpp.lines[idx] = line.replace(/DOMAIN/g, "URI_DOMAIN"); + }); + + writeToFile("./languages/cpp/include/schemas.hpp", cpp.lines); + + const go = await quicktype({ + inputData, + lang: "go", + rendererOptions: { + package: "sdk", + "just-types-and-package": true, + }, + }); + + writeToFile("./languages/go/schema.go", go.lines); + + const java = await quicktypeMultiFile({ + inputData, + lang: "java", + rendererOptions: { + package: "com.bitwarden.sdk.schema", + "java-version": "8", + }, + }); + + const javaDir = "./languages/java/src/main/java/com/bitwarden/sdk/schema/"; + if (!fs.existsSync(javaDir)) { + fs.mkdirSync(javaDir); + } + java.forEach((file, path) => { + writeToFile(javaDir + path, file.lines); + }); } main();