diff --git a/.github/renovate.json b/.github/renovate.json index 16222e52c..42f5f93f2 100644 --- a/.github/renovate.json +++ b/.github/renovate.json @@ -27,5 +27,6 @@ "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..e3abd0430 100644 --- a/.github/workflows/build-android.yml +++ b/.github/workflows/build-android.yml @@ -80,7 +80,7 @@ jobs: key: cargo-combine-cache - name: Setup Java - uses: actions/setup-java@0ab4596768b603586c0de567f2430c30f5b0d2b0 # v3.13.0 + uses: actions/setup-java@387ac29b308b003ca37ba93a6cab5eb57c8f5f93 # v4.0.0 with: distribution: temurin java-version: 17 @@ -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-dotnet.yml b/.github/workflows/build-dotnet.yml index 272fec363..7ebb03526 100644 --- a/.github/workflows/build-dotnet.yml +++ b/.github/workflows/build-dotnet.yml @@ -1,7 +1,7 @@ name: Build .NET SDK on: - pull_request: + push: branches: - master @@ -30,7 +30,7 @@ jobs: path: languages/csharp/Bitwarden.Sdk - name: Set up .NET Core - uses: actions/setup-dotnet@3447fd6a9f9e57506b15f895c5b76d3b197dc7c2 # v3.2.0 + uses: actions/setup-dotnet@4d6c8fcf3c8f7a60068d26b594648e99df24cee3 # v4.0.0 with: global-json-file: languages/csharp/global.json diff --git a/.github/workflows/build-java.yml b/.github/workflows/build-java.yml new file mode 100644 index 000000000..002164647 --- /dev/null +++ b/.github/workflows/build-java.yml @@ -0,0 +1,68 @@ +name: Build Java SDK + +on: + push: + 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@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 + + - 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@387ac29b308b003ca37ba93a6cab5eb57c8f5f93 # v4.0.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@87a9a15658c426a54dd469d4fc7dc1a73ca9d4a6 # v2.10.0 + with: + arguments: publish + build-root-directory: languages/java + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/delete-old-packages.yml b/.github/workflows/delete-old-packages.yml index 323a9a0a2..50c3a8fef 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*(-SNAPSHOT)?$' + ignore-versions: '^\d*\.\d*\.\d*(-SNAPSHOT)?$' diff --git a/.github/workflows/generate_schemas.yml b/.github/workflows/generate_schemas.yml index ef5462f8c..3caa6c26c 100644 --- a/.github/workflows/generate_schemas.yml +++ b/.github/workflows/generate_schemas.yml @@ -54,7 +54,14 @@ jobs: uses: actions/upload-artifact@a8a3f3ad30e3422c9c7b888a15615d19a852ae32 # v3.1.3 with: name: schemas.py - path: ${{ github.workspace }}/languages/python/BitwardenClient/schemas.py + path: ${{ github.workspace }}/languages/python/bitwarden_sdk/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 @@ -63,3 +70,16 @@ jobs: name: sdk-schemas-json path: ${{ github.workspace }}/support/schemas/* if-no-files-found: error + + - name: Upload Go schemas artifact + uses: actions/upload-artifact@a8a3f3ad30e3422c9c7b888a15615d19a852ae32 # v3.1.3 + 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/publish-dotnet.yml b/.github/workflows/publish-dotnet.yml index c86b90dd1..b52656de2 100644 --- a/.github/workflows/publish-dotnet.yml +++ b/.github/workflows/publish-dotnet.yml @@ -32,7 +32,7 @@ jobs: path: languages/csharp/Bitwarden.Sdk - name: Set up .NET Core - uses: actions/setup-dotnet@3447fd6a9f9e57506b15f895c5b76d3b197dc7c2 # v3.2.0 + uses: actions/setup-dotnet@4d6c8fcf3c8f7a60068d26b594648e99df24cee3 # v4.0.0 with: global-json-file: languages/csharp/global.json diff --git a/.github/workflows/publish-php.yml b/.github/workflows/publish-php.yml new file mode 100644 index 000000000..b033319ec --- /dev/null +++ b/.github/workflows/publish-php.yml @@ -0,0 +1,75 @@ +name: Publish PHP SDK + +on: + push: + branches: + - master + +jobs: + build_rust: + uses: ./.github/workflows/build-rust-cross-platform.yml + + setup_php: + name: Setup PHP + runs-on: ubuntu-22.04 + needs: + - build_rust + + steps: + - name: Checkout Repository + uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 + + - name: Setup PHP with PECL extension + uses: shivammathur/setup-php@e6f75134d35752277f093989e72e140eaa222f35 # 2.28.0 + with: + php-version: "8.0" + tools: composer + extensions: ext-ffi + + - name: Composer check + run: | + composer install + composer validate + working-directory: languages/php/ + + - 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/php/src/lib/macos-arm64 + mkdir -p languages/php/src/lib/ubuntu-x64 + mkdir -p languages/php/src/lib/macos-x64 + mkdir -p languages/php/src/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/php/src/lib/${platforms[$i]}/${files[$i]}" + done + + - name: Publish version + run: curl -XPOST -H'content-type:application/json' 'https://packagist.org/api/update-package?username=malirobot&apiToken=${{secrets.PACKAGIST_KEY}}' -d'{"repository":{"url":"https://packagist.org/packages/bitwarden/sdk"}}' + working-directory: languages/php/ diff --git a/.github/workflows/publish-ruby.yml b/.github/workflows/publish-ruby.yml new file mode 100644 index 000000000..fa46af292 --- /dev/null +++ b/.github/workflows/publish-ruby.yml @@ -0,0 +1,88 @@ +name: Publish Ruby SDK + +on: + push: + 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@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 + + - name: Set up Ruby + uses: ruby/setup-ruby@8575951200e472d5f2d95c625da0c7bec8217c42 # v1.161.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 468570cd8..010238f40 100644 --- a/.github/workflows/publish-rust-crates.yml +++ b/.github/workflows/publish-rust-crates.yml @@ -103,7 +103,7 @@ jobs: uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 - name: Login to Azure - uses: Azure/login@92a5484dfaf04ca78a94597f4f19fea633851fa2 # v1.4.7 + uses: Azure/login@de95379fe4dadc2defb305917eaa7e5dde727294 # v1.5.1 with: creds: ${{ secrets.AZURE_KV_CI_SERVICE_PRINCIPAL }} diff --git a/.github/workflows/release-cli.yml b/.github/workflows/release-cli.yml index 4c52529e8..aad7a0a46 100644 --- a/.github/workflows/release-cli.yml +++ b/.github/workflows/release-cli.yml @@ -128,7 +128,7 @@ jobs: uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 - name: Login to Azure - uses: Azure/login@92a5484dfaf04ca78a94597f4f19fea633851fa2 # v1.4.7 + uses: Azure/login@de95379fe4dadc2defb305917eaa7e5dde727294 # v1.5.1 with: creds: ${{ secrets.AZURE_KV_CI_SERVICE_PRINCIPAL }} diff --git a/.github/workflows/release-napi.yml b/.github/workflows/release-napi.yml index daa450c0b..3171cfb33 100644 --- a/.github/workflows/release-napi.yml +++ b/.github/workflows/release-napi.yml @@ -126,7 +126,7 @@ jobs: run: npm run tsc - name: Login to Azure - uses: Azure/login@92a5484dfaf04ca78a94597f4f19fea633851fa2 # v1.4.7 + uses: Azure/login@de95379fe4dadc2defb305917eaa7e5dde727294 # v1.5.1 with: creds: ${{ secrets.AZURE_KV_CI_SERVICE_PRINCIPAL }} diff --git a/.github/workflows/version-bump.yml b/.github/workflows/version-bump.yml index d902c5700..5e403ac5b 100644 --- a/.github/workflows/version-bump.yml +++ b/.github/workflows/version-bump.yml @@ -43,7 +43,7 @@ jobs: run: cargo install cargo-edit - name: Login to Azure - Prod Subscription - uses: Azure/login@92a5484dfaf04ca78a94597f4f19fea633851fa2 # v1.4.7 + uses: Azure/login@de95379fe4dadc2defb305917eaa7e5dde727294 # v1.5.1 with: creds: ${{ secrets.AZURE_KV_CI_SERVICE_PRINCIPAL }} diff --git a/.gitignore b/.gitignore index 3418c374f..23cbaba20 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,7 @@ /target .DS_Store .pytest_cache +.vscode/c_cpp_properties.json # Build results [Dd]ebug/ @@ -15,6 +16,13 @@ bld/ [Oo]bj/ *.wasm +# Binary files +*.dylib +*.a +*.so +*.dll +*.class + # Editor directories and files .idea xcuserdata/ @@ -46,9 +54,11 @@ languages/kotlin/sdk/src/main/java/com/bitwarden/sdk/bitwarden_uniffi.kt languages/kotlin/sdk/src/main/java/com/bitwarden/core/bitwarden.kt # Schemas -support/schemas crates/bitwarden-napi/src-ts/bitwarden_client/schemas.ts +languages/cpp/include/schemas.hpp languages/csharp/Bitwarden.Sdk/schemas.cs -languages/js_webassembly/bitwarden_client/schemas.ts +languages/go/schema.go +languages/java/src/main/java/com/bitwarden/sdk/schema languages/js/sdk-client/src/schemas.ts -languages/python/BitwardenClient/schemas.py +languages/python/bitwarden_sdk/schemas.py +support/schemas diff --git a/.vscode/tasks.json b/.vscode/tasks.json index fa97b8fc7..71400c06e 100644 --- a/.vscode/tasks.json +++ b/.vscode/tasks.json @@ -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 1a0cb8510..e17888496 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -203,9 +203,9 @@ dependencies = [ [[package]] name = "async-lock" -version = "3.1.2" +version = "3.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dea8b3453dd7cc96711834b75400d671b73e3656975fa68d9f277163b7f7e316" +checksum = "7125e42787d53db9dd54261812ef17e937c95a51e4d291373b670342fa44310c" dependencies = [ "event-listener 4.0.0", "event-listener-strategy", @@ -332,6 +332,7 @@ dependencies = [ "bitwarden-api-identity", "cbc", "chrono", + "data-encoding", "getrandom 0.2.11", "hkdf", "hmac", @@ -658,9 +659,9 @@ dependencies = [ [[package]] name = "clap" -version = "4.4.8" +version = "4.4.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2275f18819641850fa26c89acc84d465c1bf91ce57bc2748b28c420473352f64" +checksum = "41fffed7514f420abec6d183b1d3acfd9099c79c3a10a06ade4f8203f1411272" dependencies = [ "clap_builder", "clap_derive", @@ -668,9 +669,9 @@ dependencies = [ [[package]] name = "clap_builder" -version = "4.4.8" +version = "4.4.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "07cdf1b148b25c1e1f7a42225e30a0d99a615cd4637eae7365548dd4529b95bc" +checksum = "63361bae7eef3771745f02d8d892bec2fee5f6e34af316ba556e7f97a7069ff1" dependencies = [ "anstream", "anstyle", @@ -764,9 +765,9 @@ dependencies = [ [[package]] name = "concurrent-queue" -version = "2.3.0" +version = "2.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f057a694a54f12365049b0958a1685bb52d567f5593b355fbf685838e873d400" +checksum = "d16048cd947b08fa32c24458a22f5dc5e835264f689f4f5653210c69fd107363" dependencies = [ "crossbeam-utils", ] @@ -831,9 +832,9 @@ dependencies = [ [[package]] name = "core-foundation" -version = "0.9.3" +version = "0.9.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "194a7a9e6de53fa55116934067c844d9d749312f75c6f6d0980e8c252f8c2146" +checksum = "91e195e091a93c46f7102ec7818a2aa394e1e1771c3ab4825963fa03e45afb8f" dependencies = [ "core-foundation-sys", "libc", @@ -841,9 +842,9 @@ dependencies = [ [[package]] name = "core-foundation-sys" -version = "0.8.4" +version = "0.8.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e496a50fda8aacccc86d7529e2c1e0892dbd0f898a6b5645b5561b89c3210efa" +checksum = "06ea2b9bc92be3c2baa9334a323ebca2d6f074ff852cd1d7b11064035cd3868f" [[package]] name = "cpufeatures" @@ -930,6 +931,12 @@ dependencies = [ "syn 2.0.39", ] +[[package]] +name = "data-encoding" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e962a19be5cfc3f3bf6dd8f61eb50107f356ad6270fbb3ed41476571db78be5" + [[package]] name = "deadpool" version = "0.9.5" @@ -962,9 +969,9 @@ dependencies = [ [[package]] name = "deranged" -version = "0.3.9" +version = "0.3.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0f32d04922c60427da6f9fef14d042d9edddef64cb9d4ce0d64d0685fbeb1fd3" +checksum = "8eb30d70a07a3b04884d2677f06bec33509dc67ca60d92949e5535352d3191dc" dependencies = [ "powerfmt", ] @@ -1050,12 +1057,12 @@ checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" [[package]] name = "errno" -version = "0.3.7" +version = "0.3.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f258a7194e7f7c2a7837a8913aeab7fd8c383457034fa20ce4dd3dcb813e8eb8" +checksum = "a258e46cdc063eb8519c00b9fc845fc47bcfca4130e2f08e88665ceda8474245" dependencies = [ "libc", - "windows-sys 0.48.0", + "windows-sys 0.52.0", ] [[package]] @@ -1143,18 +1150,18 @@ checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b" [[package]] name = "form_urlencoded" -version = "1.2.0" +version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a62bc1cf6f830c2ec14a513a9fb124d0a213a629668a4186f329db21fe045652" +checksum = "e13624c2627564efccf4934284bdd98cbaa14e79b0b5a141218e507b3a823456" dependencies = [ "percent-encoding", ] [[package]] name = "fs-err" -version = "2.10.0" +version = "2.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fb5fd9bcbe8b1087cbd395b51498c01bc997cef73e778a80b77a811af5e2d29f" +checksum = "88a41f105fe1d5b6b34b2055e3dc59bb79b46b48b2040b9e6c7b4b5de097aa41" dependencies = [ "autocfg", ] @@ -1318,9 +1325,9 @@ dependencies = [ [[package]] name = "gimli" -version = "0.28.0" +version = "0.28.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6fb8d784f27acf97159b40fc4db5ecd8aa23b9ad5ef69cdd136d3bc80665f0c0" +checksum = "4271d37baee1b8c7e4b708028c57d816cf9d2434acb33a549475f78c181f6253" [[package]] name = "glob" @@ -1330,15 +1337,15 @@ checksum = "d2fabcfbdc87f4758337ca535fb41a6d701b65693ce38287d856d1674551ec9b" [[package]] name = "globset" -version = "0.4.13" +version = "0.4.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "759c97c1e17c55525b57192c06a267cda0ac5210b222d6b82189a2338fa1c13d" +checksum = "57da3b9b5b85bd66f31093f8c408b90a74431672542466497dcbdfdc02034be1" dependencies = [ "aho-corasick", "bstr", - "fnv", "log", - "regex", + "regex-automata 0.4.3", + "regex-syntax 0.8.2", ] [[package]] @@ -1379,9 +1386,9 @@ checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" [[package]] name = "hashbrown" -version = "0.14.2" +version = "0.14.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f93e7192158dbcda357bdec5fb5788eebf8bbac027f3f33e719d29135ae84156" +checksum = "290f1a1d9242c78d09ce40a5e87e7554ee637af1351968159f4952f028f75604" [[package]] name = "heck" @@ -1545,9 +1552,9 @@ dependencies = [ [[package]] name = "idna" -version = "0.4.0" +version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7d20d6b07bfbc108882d88ed8e37d39636dcc260e15e30c45e6ba089610b917c" +checksum = "634d9b1461af396cad843f47fdba5597a4f9e6ddd4bfb6ff5d85028c25cb12f6" dependencies = [ "unicode-bidi", "unicode-normalization", @@ -1577,7 +1584,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d530e1a18b1cb4c484e6e34556a0d948706958449fca0cab753d649f2bce3d1f" dependencies = [ "equivalent", - "hashbrown 0.14.2", + "hashbrown 0.14.3", ] [[package]] @@ -1670,9 +1677,9 @@ checksum = "af150ab688ff2122fcef229be89cb50dd66af9e01a4ff320cc137eecc9bacc38" [[package]] name = "js-sys" -version = "0.3.64" +version = "0.3.66" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c5f195fe497f702db0f318b07fdd68edb16955aed830df8363d837542f8f935a" +checksum = "cee9c64da59eae3b50095c18d3e74f8b73c0b86d2792824ff01bbce68ba229ca" dependencies = [ "wasm-bindgen", ] @@ -1730,9 +1737,9 @@ dependencies = [ [[package]] name = "linux-raw-sys" -version = "0.4.11" +version = "0.4.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "969488b55f8ac402214f3f5fd243ebb7206cf82de60d3172994707a4bcc2b829" +checksum = "c4cd1a83af159aa67994778be9070f0ae1bd732942279cabb14f86f986a21456" [[package]] name = "lock_api" @@ -2062,9 +2069,9 @@ dependencies = [ [[package]] name = "openssl" -version = "0.10.59" +version = "0.10.60" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7a257ad03cd8fb16ad4172fedf8094451e1af1c4b70097636ef2eac9a5f0cc33" +checksum = "79a4c6c3a2b158f7f8f2a2fc5a969fa3a068df6fc9dbb4a43845436e3af7c800" dependencies = [ "bitflags 2.4.1", "cfg-if", @@ -2103,9 +2110,9 @@ dependencies = [ [[package]] name = "openssl-sys" -version = "0.9.95" +version = "0.9.96" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "40a4130519a360279579c2053038317e40eff64d13fd3f004f9e1b72b8a6aaf9" +checksum = "3812c071ba60da8b5677cc12bcb1d42989a65553772897a7e0355545a819838f" dependencies = [ "cc", "libc", @@ -2207,9 +2214,9 @@ dependencies = [ [[package]] name = "percent-encoding" -version = "2.3.0" +version = "2.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9b2a4787296e9989611394c33f193f676704af1686e70b8f8033ab5ba9a35a94" +checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e" [[package]] name = "pin-project-lite" @@ -2284,9 +2291,9 @@ checksum = "5b40af805b3121feab8a3c29f04d8ad262fa8e0561883e7653e024ae4479e6de" [[package]] name = "proc-macro2" -version = "1.0.69" +version = "1.0.70" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "134c189feb4956b20f6f547d2cf727d4c0fe06722b20a0eec87ed445a97f92da" +checksum = "39278fbbf5fb4f646ce651690877f89d1c5811a3d4acb27700c1cb3cdb78fd3b" dependencies = [ "unicode-ident", ] @@ -2603,9 +2610,9 @@ dependencies = [ [[package]] name = "rsa" -version = "0.9.3" +version = "0.9.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "86ef35bf3e7fe15a53c4ab08a998e42271eab13eb0db224126bc7bc4c4bad96d" +checksum = "5d0e5124fcb30e76a7e79bfee683a2746db83784b86289f6251b54b7950a0dfc" dependencies = [ "const-oid", "digest", @@ -2629,15 +2636,15 @@ checksum = "d626bb9dae77e28219937af045c257c28bfd3f69333c512553507f5f9798cb76" [[package]] name = "rustix" -version = "0.38.25" +version = "0.38.26" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dc99bc2d4f1fed22595588a013687477aedf3cdcfb26558c559edb67b4d9b22e" +checksum = "9470c4bf8246c8daf25f9598dca807fb6510347b1e1cfa55749113850c79d88a" dependencies = [ "bitflags 2.4.1", "errno", "libc", "linux-raw-sys", - "windows-sys 0.48.0", + "windows-sys 0.52.0", ] [[package]] @@ -2981,6 +2988,12 @@ 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 = "b7c388c1b5e93756d0c740965c41e8822f866621d41acbdf6336a6a168f8840c" + [[package]] name = "socket2" version = "0.4.10" @@ -3009,9 +3022,9 @@ checksum = "6e63cff320ae2c57904679ba7cb63280a3dc4613885beafb148ee7bf9aa9042d" [[package]] name = "spki" -version = "0.7.2" +version = "0.7.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9d1e996ef02c474957d681f1b05213dfb0abab947b446a62d37770b23500184a" +checksum = "d91ed6c858b01f942cd56b37a94b3e0a1798290327d1236e4d9cf4eaca44d29d" dependencies = [ "base64ct", "der", @@ -3160,6 +3173,17 @@ 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" @@ -3439,6 +3463,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" @@ -3462,8 +3492,8 @@ checksum = "e51733f11c9c4f72aa0c160008246859e340b00807569a0da0e7a1079b27ba85" [[package]] name = "uniffi" -version = "0.25.1" -source = "git+https://github.com/mozilla/uniffi-rs?rev=0a03b713306d6ce3de033157fc2ce92a238c2e24#0a03b713306d6ce3de033157fc2ce92a238c2e24" +version = "0.25.2" +source = "git+https://github.com/mozilla/uniffi-rs?rev=23711c8151bbb794369aa1f9d383db386792dff9#23711c8151bbb794369aa1f9d383db386792dff9" dependencies = [ "anyhow", "camino", @@ -3483,8 +3513,8 @@ dependencies = [ [[package]] name = "uniffi_bindgen" -version = "0.25.1" -source = "git+https://github.com/mozilla/uniffi-rs?rev=0a03b713306d6ce3de033157fc2ce92a238c2e24#0a03b713306d6ce3de033157fc2ce92a238c2e24" +version = "0.25.2" +source = "git+https://github.com/mozilla/uniffi-rs?rev=23711c8151bbb794369aa1f9d383db386792dff9#23711c8151bbb794369aa1f9d383db386792dff9" dependencies = [ "anyhow", "askama", @@ -3498,6 +3528,7 @@ dependencies = [ "once_cell", "paste", "serde", + "textwrap", "toml 0.5.11", "uniffi_meta", "uniffi_testing", @@ -3506,8 +3537,8 @@ dependencies = [ [[package]] name = "uniffi_build" -version = "0.25.1" -source = "git+https://github.com/mozilla/uniffi-rs?rev=0a03b713306d6ce3de033157fc2ce92a238c2e24#0a03b713306d6ce3de033157fc2ce92a238c2e24" +version = "0.25.2" +source = "git+https://github.com/mozilla/uniffi-rs?rev=23711c8151bbb794369aa1f9d383db386792dff9#23711c8151bbb794369aa1f9d383db386792dff9" dependencies = [ "anyhow", "camino", @@ -3516,8 +3547,8 @@ dependencies = [ [[package]] name = "uniffi_checksum_derive" -version = "0.25.1" -source = "git+https://github.com/mozilla/uniffi-rs?rev=0a03b713306d6ce3de033157fc2ce92a238c2e24#0a03b713306d6ce3de033157fc2ce92a238c2e24" +version = "0.25.2" +source = "git+https://github.com/mozilla/uniffi-rs?rev=23711c8151bbb794369aa1f9d383db386792dff9#23711c8151bbb794369aa1f9d383db386792dff9" dependencies = [ "quote", "syn 2.0.39", @@ -3525,8 +3556,8 @@ dependencies = [ [[package]] name = "uniffi_core" -version = "0.25.1" -source = "git+https://github.com/mozilla/uniffi-rs?rev=0a03b713306d6ce3de033157fc2ce92a238c2e24#0a03b713306d6ce3de033157fc2ce92a238c2e24" +version = "0.25.2" +source = "git+https://github.com/mozilla/uniffi-rs?rev=23711c8151bbb794369aa1f9d383db386792dff9#23711c8151bbb794369aa1f9d383db386792dff9" dependencies = [ "anyhow", "bytes", @@ -3540,8 +3571,8 @@ dependencies = [ [[package]] name = "uniffi_macros" -version = "0.25.1" -source = "git+https://github.com/mozilla/uniffi-rs?rev=0a03b713306d6ce3de033157fc2ce92a238c2e24#0a03b713306d6ce3de033157fc2ce92a238c2e24" +version = "0.25.2" +source = "git+https://github.com/mozilla/uniffi-rs?rev=23711c8151bbb794369aa1f9d383db386792dff9#23711c8151bbb794369aa1f9d383db386792dff9" dependencies = [ "bincode", "camino", @@ -3558,8 +3589,8 @@ dependencies = [ [[package]] name = "uniffi_meta" -version = "0.25.1" -source = "git+https://github.com/mozilla/uniffi-rs?rev=0a03b713306d6ce3de033157fc2ce92a238c2e24#0a03b713306d6ce3de033157fc2ce92a238c2e24" +version = "0.25.2" +source = "git+https://github.com/mozilla/uniffi-rs?rev=23711c8151bbb794369aa1f9d383db386792dff9#23711c8151bbb794369aa1f9d383db386792dff9" dependencies = [ "anyhow", "bytes", @@ -3569,8 +3600,8 @@ dependencies = [ [[package]] name = "uniffi_testing" -version = "0.25.1" -source = "git+https://github.com/mozilla/uniffi-rs?rev=0a03b713306d6ce3de033157fc2ce92a238c2e24#0a03b713306d6ce3de033157fc2ce92a238c2e24" +version = "0.25.2" +source = "git+https://github.com/mozilla/uniffi-rs?rev=23711c8151bbb794369aa1f9d383db386792dff9#23711c8151bbb794369aa1f9d383db386792dff9" dependencies = [ "anyhow", "camino", @@ -3581,10 +3612,11 @@ dependencies = [ [[package]] name = "uniffi_udl" -version = "0.25.1" -source = "git+https://github.com/mozilla/uniffi-rs?rev=0a03b713306d6ce3de033157fc2ce92a238c2e24#0a03b713306d6ce3de033157fc2ce92a238c2e24" +version = "0.25.2" +source = "git+https://github.com/mozilla/uniffi-rs?rev=23711c8151bbb794369aa1f9d383db386792dff9#23711c8151bbb794369aa1f9d383db386792dff9" dependencies = [ "anyhow", + "textwrap", "uniffi_meta", "uniffi_testing", "weedle2", @@ -3604,9 +3636,9 @@ checksum = "f28467d3e1d3c6586d8f25fa243f544f5800fec42d97032474e17222c2b75cfa" [[package]] name = "url" -version = "2.4.1" +version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "143b538f18257fac9cad154828a57c6bf5157e1aa604d4816b5995bf6de87ae5" +checksum = "31e6302e3bb753d46e83516cae55ae196fc0c309407cf11ab35cc51a4c2a4633" dependencies = [ "form_urlencoded", "idna", @@ -3686,9 +3718,9 @@ checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" [[package]] name = "wasm-bindgen" -version = "0.2.87" +version = "0.2.89" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7706a72ab36d8cb1f80ffbf0e071533974a60d0a308d01a5d0375bf60499a342" +checksum = "0ed0d4f68a3015cc185aff4db9506a015f4b96f95303897bfa23f846db54064e" dependencies = [ "cfg-if", "serde", @@ -3698,9 +3730,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-backend" -version = "0.2.87" +version = "0.2.89" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5ef2b6d3c510e9625e5fe6f509ab07d66a760f0885d858736483c32ed7809abd" +checksum = "1b56f625e64f3a1084ded111c4d5f477df9f8c92df113852fa5a374dbda78826" dependencies = [ "bumpalo", "log", @@ -3713,9 +3745,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-futures" -version = "0.4.37" +version = "0.4.39" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c02dbc21516f9f1f04f187958890d7e6026df8d16540b7ad9492bc34a67cea03" +checksum = "ac36a15a220124ac510204aec1c3e5db8a22ab06fd6706d881dc6149f8ed9a12" dependencies = [ "cfg-if", "js-sys", @@ -3725,9 +3757,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro" -version = "0.2.87" +version = "0.2.89" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dee495e55982a3bd48105a7b947fd2a9b4a8ae3010041b9e0faab3f9cd028f1d" +checksum = "0162dbf37223cd2afce98f3d0785506dcb8d266223983e4b5b525859e6e182b2" dependencies = [ "quote", "wasm-bindgen-macro-support", @@ -3735,9 +3767,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.87" +version = "0.2.89" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "54681b18a46765f095758388f2d0cf16eb8d4169b639ab575a8f5693af210c7b" +checksum = "f0eb82fcb7930ae6219a7ecfd55b217f5f0893484b7a13022ebb2b2bf20b5283" dependencies = [ "proc-macro2", "quote", @@ -3748,15 +3780,15 @@ dependencies = [ [[package]] name = "wasm-bindgen-shared" -version = "0.2.87" +version = "0.2.89" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ca6ad05a4870b2bf5fe995117d3728437bd27d7cd5f06f13c17443ef369775a1" +checksum = "7ab9b36309365056cd639da3134bf87fa8f3d86008abf99e612384a6eecd459f" [[package]] name = "wasm-bindgen-test" -version = "0.3.37" +version = "0.3.39" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6e6e302a7ea94f83a6d09e78e7dc7d9ca7b186bc2829c24a22d0753efd680671" +checksum = "2cf9242c0d27999b831eae4767b2a146feb0b27d332d553e605864acd2afd403" dependencies = [ "console_error_panic_hook", "js-sys", @@ -3768,19 +3800,20 @@ dependencies = [ [[package]] name = "wasm-bindgen-test-macro" -version = "0.3.37" +version = "0.3.39" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ecb993dd8c836930ed130e020e77d9b2e65dd0fbab1b67c790b0f5d80b11a575" +checksum = "794645f5408c9a039fd09f4d113cdfb2e7eba5ff1956b07bcf701cf4b394fe89" dependencies = [ "proc-macro2", "quote", + "syn 2.0.39", ] [[package]] name = "web-sys" -version = "0.3.64" +version = "0.3.66" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9b85cbef8c220a6abc02aefd892dfc0fc23afb1c6a426316ec33253a3877249b" +checksum = "50c24a44ec86bb68fbecd1b3efed7e85ea5621b39b35ef2766b66cd984f8010f" dependencies = [ "js-sys", "wasm-bindgen", @@ -3789,7 +3822,7 @@ dependencies = [ [[package]] name = "weedle2" version = "4.0.0" -source = "git+https://github.com/mozilla/uniffi-rs?rev=0a03b713306d6ce3de033157fc2ce92a238c2e24#0a03b713306d6ce3de033157fc2ce92a238c2e24" +source = "git+https://github.com/mozilla/uniffi-rs?rev=23711c8151bbb794369aa1f9d383db386792dff9#23711c8151bbb794369aa1f9d383db386792dff9" dependencies = [ "nom", ] @@ -3861,6 +3894,15 @@ dependencies = [ "windows-targets 0.48.5", ] +[[package]] +name = "windows-sys" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" +dependencies = [ + "windows-targets 0.52.0", +] + [[package]] name = "windows-targets" version = "0.42.2" @@ -3891,6 +3933,21 @@ dependencies = [ "windows_x86_64_msvc 0.48.5", ] +[[package]] +name = "windows-targets" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a18201040b24831fbb9e4eb208f8892e1f50a37feb53cc7ff887feb8f50e7cd" +dependencies = [ + "windows_aarch64_gnullvm 0.52.0", + "windows_aarch64_msvc 0.52.0", + "windows_i686_gnu 0.52.0", + "windows_i686_msvc 0.52.0", + "windows_x86_64_gnu 0.52.0", + "windows_x86_64_gnullvm 0.52.0", + "windows_x86_64_msvc 0.52.0", +] + [[package]] name = "windows_aarch64_gnullvm" version = "0.42.2" @@ -3903,6 +3960,12 @@ version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb7764e35d4db8a7921e09562a0304bf2f93e0a51bfccee0bd0bb0b666b015ea" + [[package]] name = "windows_aarch64_msvc" version = "0.42.2" @@ -3915,6 +3978,12 @@ version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbaa0368d4f1d2aaefc55b6fcfee13f41544ddf36801e793edbbfd7d7df075ef" + [[package]] name = "windows_i686_gnu" version = "0.42.2" @@ -3927,6 +3996,12 @@ version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" +[[package]] +name = "windows_i686_gnu" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a28637cb1fa3560a16915793afb20081aba2c92ee8af57b4d5f28e4b3e7df313" + [[package]] name = "windows_i686_msvc" version = "0.42.2" @@ -3939,6 +4014,12 @@ version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" +[[package]] +name = "windows_i686_msvc" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ffe5e8e31046ce6230cc7215707b816e339ff4d4d67c65dffa206fd0f7aa7b9a" + [[package]] name = "windows_x86_64_gnu" version = "0.42.2" @@ -3951,6 +4032,12 @@ version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" +[[package]] +name = "windows_x86_64_gnu" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d6fa32db2bc4a2f5abeacf2b69f7992cd09dca97498da74a151a3132c26befd" + [[package]] name = "windows_x86_64_gnullvm" version = "0.42.2" @@ -3963,6 +4050,12 @@ version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a657e1e9d3f514745a572a6846d3c7aa7dbe1658c056ed9c3344c4109a6949e" + [[package]] name = "windows_x86_64_msvc" version = "0.42.2" @@ -3975,6 +4068,12 @@ version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dff9641d1cd4be8d1a070daf9e3773c5f67e78b4d9d42263020c057706765c04" + [[package]] name = "winnow" version = "0.5.19" @@ -3996,9 +4095,9 @@ dependencies = [ [[package]] name = "wiremock" -version = "0.5.21" +version = "0.5.22" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "079aee011e8a8e625d16df9e785de30a6b77f80a6126092d76a57375f96448da" +checksum = "13a3a53eaf34f390dd30d7b1b078287dd05df2aa2e21a589ccb80f5c7253c2e9" dependencies = [ "assert-json-diff", "async-trait", diff --git a/Cargo.toml b/Cargo.toml index c7c2906dc..f841f07c9 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -24,8 +24,8 @@ codegen-units = 1 # 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 = "0a03b713306d6ce3de033157fc2ce92a238c2e24" } -uniffi_build = { git = "https://github.com/mozilla/uniffi-rs", rev = "0a03b713306d6ce3de033157fc2ce92a238c2e24" } -uniffi_bindgen = { git = "https://github.com/mozilla/uniffi-rs", rev = "0a03b713306d6ce3de033157fc2ce92a238c2e24" } -uniffi_core = { git = "https://github.com/mozilla/uniffi-rs", rev = "0a03b713306d6ce3de033157fc2ce92a238c2e24" } -uniffi_macros = { git = "https://github.com/mozilla/uniffi-rs", rev = "0a03b713306d6ce3de033157fc2ce92a238c2e24" } +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 1d4dabb80..8ff164e88 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/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-cli/Cargo.toml b/crates/bitwarden-cli/Cargo.toml index 0966f7885..86ca2fac1 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.4.8", features = ["derive"] } +clap = { version = "4.4.10", features = ["derive"] } color-eyre = "0.6" inquire = "0.6.2" supports-color = "2.1.0" diff --git a/crates/bitwarden-napi/package-lock.json b/crates/bitwarden-napi/package-lock.json index 3ccf3a7cb..df8824a29 100644 --- a/crates/bitwarden-napi/package-lock.json +++ b/crates/bitwarden-napi/package-lock.json @@ -95,9 +95,9 @@ "dev": true }, "node_modules/@types/node": { - "version": "20.9.2", - "resolved": "https://registry.npmjs.org/@types/node/-/node-20.9.2.tgz", - "integrity": "sha512-WHZXKFCEyIUJzAwh3NyyTHYSR35SevJ6mZ1nWwJafKtiQbqRTIKSRcw3Ma3acqgsent3RRDqeVwpHntMk+9irg==", + "version": "20.10.3", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.10.3.tgz", + "integrity": "sha512-XJavIpZqiXID5Yxnxv3RUDKTN5b81ddNC3ecsA0SoFXz/QU8OGBwZGMomiq0zw+uuqbL/krztv/DINAQ/EV4gg==", "dev": true, "peer": true, "dependencies": { @@ -196,9 +196,9 @@ } }, "node_modules/typescript": { - "version": "5.2.2", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.2.2.tgz", - "integrity": "sha512-mI4WrpHsbCIcwT9cF4FZvr80QUeKvsUsUvKDoR+X/7XHQH98xYD8YHZg7ANtz2GtZt/CBq2QJ0thkGJMHfqc1w==", + "version": "5.3.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.3.3.tgz", + "integrity": "sha512-pXWcraxM0uxAS+tN0AG/BF2TyqmHO014Z070UsJ+pFvYuRSq8KH8DmWpnbXe0pEPDHXZV3FcAbJkijJ5oNEnWw==", "dev": true, "bin": { "tsc": "bin/tsc", diff --git a/crates/bitwarden-uniffi/Cargo.toml b/crates/bitwarden-uniffi/Cargo.toml index 405642d71..ecde30a4b 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.1.2" +async-lock = "3.2.0" chrono = { version = ">=0.4.26, <0.5", features = [ "serde", "std", ], default-features = false } env_logger = "0.10.1" schemars = { version = ">=0.8, <0.9", optional = true } -uniffi = "=0.25.1" +uniffi = "=0.25.2" bitwarden = { path = "../bitwarden", features = ["mobile", "internal"] } [build-dependencies] -uniffi = { version = "=0.25.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/src/docs.rs b/crates/bitwarden-uniffi/src/docs.rs index 45fabb0b7..d66c78002 100644 --- a/crates/bitwarden-uniffi/src/docs.rs +++ b/crates/bitwarden-uniffi/src/docs.rs @@ -2,6 +2,7 @@ use bitwarden::{ auth::password::MasterPasswordPolicyOptions, client::kdf::Kdf, mobile::crypto::{InitOrgCryptoRequest, InitUserCryptoRequest}, + platform::FingerprintRequest, tool::{ExportFormat, PassphraseGeneratorRequest, PasswordGeneratorRequest}, vault::{ Cipher, CipherView, Collection, Folder, FolderView, Send, SendListView, SendView, @@ -34,6 +35,9 @@ pub enum DocRef { // Exporters ExportFormat(ExportFormat), + // Platform + FingerprintRequest(FingerprintRequest), + // Auth MasterPasswordPolicyOptions(MasterPasswordPolicyOptions), diff --git a/crates/bitwarden-uniffi/src/lib.rs b/crates/bitwarden-uniffi/src/lib.rs index 5035f22b9..13253d9a6 100644 --- a/crates/bitwarden-uniffi/src/lib.rs +++ b/crates/bitwarden-uniffi/src/lib.rs @@ -9,6 +9,7 @@ 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; @@ -18,6 +19,7 @@ pub mod docs; use crypto::ClientCrypto; use error::Result; +use platform::ClientPlatform; use tool::ClientGenerators; use vault::ClientVault; @@ -42,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)) 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/vault/mod.rs b/crates/bitwarden-uniffi/src/vault/mod.rs index 79b3e5835..bd5f5332c 100644 --- a/crates/bitwarden-uniffi/src/vault/mod.rs +++ b/crates/bitwarden-uniffi/src/vault/mod.rs @@ -3,7 +3,7 @@ use std::sync::Arc; use bitwarden::vault::TotpResponse; use chrono::{DateTime, Utc}; -use crate::Client; +use crate::{error::Result, Client}; pub mod ciphers; pub mod collections; @@ -47,13 +47,11 @@ impl ClientVault { /// - A base32 encoded string /// - OTP Auth URI /// - Steam URI - pub async fn generate_totp(&self, key: String, time: Option>) -> TotpResponse { - self.0 - .0 - .read() - .await - .vault() - .generate_totp(key, time) - .await + pub async fn generate_totp( + &self, + key: String, + time: Option>, + ) -> Result { + Ok(self.0 .0.read().await.vault().generate_totp(key, time)?) } } diff --git a/crates/bitwarden-wasm/Cargo.toml b/crates/bitwarden-wasm/Cargo.toml index 956372678..176edeaa9 100644 --- a/crates/bitwarden-wasm/Cargo.toml +++ b/crates/bitwarden-wasm/Cargo.toml @@ -10,11 +10,11 @@ crate-type = ["cdylib"] [dependencies] console_error_panic_hook = "0.1.7" console_log = { version = "1.0.0", features = ["color"] } -js-sys = "0.3.63" +js-sys = "0.3.66" 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" +wasm-bindgen = { version = "0.2.89", features = ["serde-serialize"] } +wasm-bindgen-futures = "0.4.39" bitwarden-json = { path = "../bitwarden-json", features = [ "secrets", @@ -22,4 +22,4 @@ bitwarden-json = { path = "../bitwarden-json", features = [ ] } [dev-dependencies] -wasm-bindgen-test = "0.3.36" +wasm-bindgen-test = "0.3.39" diff --git a/crates/bitwarden/Cargo.toml b/crates/bitwarden/Cargo.toml index d1bfc99cc..40d759ea6 100644 --- a/crates/bitwarden/Cargo.toml +++ b/crates/bitwarden/Cargo.toml @@ -32,9 +32,11 @@ 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 = [ + "clock", "serde", "std", ], default-features = false } +data-encoding = ">=2.5.0, <3.0" # 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" @@ -57,10 +59,10 @@ sha1 = ">=0.10.5, <0.11" sha2 = ">=0.10.6, <0.11" subtle = ">=2.5.0, <3.0" thiserror = ">=1.0.40, <2.0" -uniffi = { version = "=0.25.1", optional = true } +uniffi = { version = "=0.25.2", optional = true } uuid = { version = ">=1.3.3, <2.0", features = ["serde"] } [dev-dependencies] rand_chacha = "0.3.1" tokio = { version = "1.34.0", features = ["rt", "macros"] } -wiremock = "0.5.21" +wiremock = "0.5.22" diff --git a/crates/bitwarden/src/auth/renew.rs b/crates/bitwarden/src/auth/renew.rs index 3b467a232..a8bfac51f 100644 --- a/crates/bitwarden/src/auth/renew.rs +++ b/crates/bitwarden/src/auth/renew.rs @@ -1,6 +1,4 @@ -use std::time::Duration; - -use instant::Instant; +use chrono::Utc; #[cfg(feature = "internal")] use crate::{auth::api::request::ApiTokenRequest, client::UserLoginMethod}; @@ -11,10 +9,10 @@ use crate::{ }; pub(crate) async fn renew_token(client: &mut Client) -> Result<()> { - const TOKEN_RENEW_MARGIN: Duration = Duration::from_secs(5 * 60); + const TOKEN_RENEW_MARGIN_SECONDS: i64 = 5 * 60; if let (Some(expires), Some(login_method)) = (&client.token_expires_in, &client.login_method) { - if expires > &(Instant::now() + TOKEN_RENEW_MARGIN) { + if Utc::now().timestamp() < expires - TOKEN_RENEW_MARGIN_SECONDS { return Ok(()); } diff --git a/crates/bitwarden/src/client/client.rs b/crates/bitwarden/src/client/client.rs index 7ee273bbd..6d1503312 100644 --- a/crates/bitwarden/src/client/client.rs +++ b/crates/bitwarden/src/client/client.rs @@ -1,6 +1,4 @@ -use std::time::Duration; - -use instant::Instant; +use chrono::Utc; use reqwest::header::{self}; use uuid::Uuid; @@ -70,7 +68,7 @@ pub(crate) enum ServiceAccountLoginMethod { pub struct Client { token: Option, pub(crate) refresh_token: Option, - pub(crate) token_expires_in: Option, + pub(crate) token_expires_in: Option, pub(crate) login_method: Option, /// Use Client::get_api_configurations() to access this. @@ -191,7 +189,7 @@ impl Client { ) { self.token = Some(token.clone()); self.refresh_token = refresh_token; - self.token_expires_in = Some(Instant::now() + Duration::from_secs(expires_in)); + self.token_expires_in = Some(Utc::now().timestamp() + expires_in as i64); self.login_method = Some(login_method); self.__api_configurations.identity.oauth_access_token = Some(token.clone()); self.__api_configurations.api.oauth_access_token = Some(token); @@ -263,7 +261,7 @@ impl Client { } #[cfg(feature = "internal")] - pub fn fingerprint(&mut self, input: &FingerprintRequest) -> Result { + pub fn fingerprint(&self, input: &FingerprintRequest) -> Result { generate_fingerprint(input) } } diff --git a/crates/bitwarden/src/mobile/vault/client_totp.rs b/crates/bitwarden/src/mobile/vault/client_totp.rs index 97eb243af..75bfd204c 100644 --- a/crates/bitwarden/src/mobile/vault/client_totp.rs +++ b/crates/bitwarden/src/mobile/vault/client_totp.rs @@ -1,5 +1,6 @@ use chrono::{DateTime, Utc}; +use crate::error::Result; use crate::vault::{generate_totp, TotpResponse}; use super::client_vault::ClientVault; @@ -12,7 +13,11 @@ impl<'a> ClientVault<'a> { /// - OTP Auth URI /// - Steam URI /// - pub async fn generate_totp(&'a self, key: String, time: Option>) -> TotpResponse { - generate_totp(key, time).await + pub fn generate_totp( + &'a self, + key: String, + time: Option>, + ) -> Result { + generate_totp(key, time) } } 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/tool/generators/client_generator.rs b/crates/bitwarden/src/tool/generators/client_generator.rs index 0384eec6a..4675ad99d 100644 --- a/crates/bitwarden/src/tool/generators/client_generator.rs +++ b/crates/bitwarden/src/tool/generators/client_generator.rs @@ -1,8 +1,7 @@ use crate::{ error::Result, - tool::generators::password::{ - passphrase, password, PassphraseGeneratorRequest, PasswordGeneratorRequest, - }, + tool::generators::passphrase::{passphrase, PassphraseGeneratorRequest}, + tool::generators::password::{password, PasswordGeneratorRequest}, Client, }; @@ -11,10 +10,52 @@ pub struct ClientGenerator<'a> { } impl<'a> ClientGenerator<'a> { + /// Generates a random password. + /// + /// The character sets and password length can be customized using the `input` parameter. + /// + /// # Examples + /// + /// ``` + /// use bitwarden::{Client, tool::PasswordGeneratorRequest, error::Result}; + /// async fn test() -> Result<()> { + /// let input = PasswordGeneratorRequest { + /// lowercase: true, + /// uppercase: true, + /// numbers: true, + /// length: 20, + /// ..Default::default() + /// }; + /// let password = Client::new(None).generator().password(input).await.unwrap(); + /// println!("{}", password); + /// Ok(()) + /// } + /// ``` pub async fn password(&self, input: PasswordGeneratorRequest) -> Result { password(input) } + /// Generates a random passphrase. + /// A passphrase is a combination of random words separated by a character. + /// An example of passphrase is `correct horse battery staple`. + /// + /// The number of words and their case, the word separator, and the inclusion of + /// a number in the passphrase can be customized using the `input` parameter. + /// + /// # Examples + /// + /// ``` + /// use bitwarden::{Client, tool::PassphraseGeneratorRequest, error::Result}; + /// async fn test() -> Result<()> { + /// let input = PassphraseGeneratorRequest { + /// num_words: 4, + /// ..Default::default() + /// }; + /// let passphrase = Client::new(None).generator().passphrase(input).await.unwrap(); + /// println!("{}", passphrase); + /// Ok(()) + /// } + /// ``` pub async fn passphrase(&self, input: PassphraseGeneratorRequest) -> Result { passphrase(input) } diff --git a/crates/bitwarden/src/tool/generators/mod.rs b/crates/bitwarden/src/tool/generators/mod.rs index 4d991b765..12c526930 100644 --- a/crates/bitwarden/src/tool/generators/mod.rs +++ b/crates/bitwarden/src/tool/generators/mod.rs @@ -1,5 +1,7 @@ mod client_generator; +mod passphrase; mod password; pub use client_generator::ClientGenerator; -pub use password::{PassphraseGeneratorRequest, PasswordGeneratorRequest}; +pub use passphrase::PassphraseGeneratorRequest; +pub use password::PasswordGeneratorRequest; diff --git a/crates/bitwarden/src/tool/generators/passphrase.rs b/crates/bitwarden/src/tool/generators/passphrase.rs new file mode 100644 index 000000000..e62fa8f9a --- /dev/null +++ b/crates/bitwarden/src/tool/generators/passphrase.rs @@ -0,0 +1,236 @@ +use crate::{ + error::{Error, Result}, + wordlist::EFF_LONG_WORD_LIST, +}; +use rand::{seq::SliceRandom, Rng, RngCore}; +use schemars::JsonSchema; +use serde::{Deserialize, Serialize}; + +/// Passphrase generator request options. +#[derive(Serialize, Deserialize, Debug, JsonSchema)] +#[serde(rename_all = "camelCase", deny_unknown_fields)] +#[cfg_attr(feature = "mobile", derive(uniffi::Record))] +pub struct PassphraseGeneratorRequest { + /// Number of words in the generated passphrase. + /// This value must be between 3 and 20. + pub num_words: u8, + /// Character separator between words in the generated passphrase. The value cannot be empty. + pub word_separator: String, + /// When set to true, capitalize the first letter of each word in the generated passphrase. + pub capitalize: bool, + /// When set to true, include a number at the end of one of the words in the generated passphrase. + pub include_number: bool, +} + +impl Default for PassphraseGeneratorRequest { + fn default() -> Self { + Self { + num_words: 3, + word_separator: ' '.to_string(), + capitalize: false, + include_number: false, + } + } +} + +const MINIMUM_PASSPHRASE_NUM_WORDS: u8 = 3; +const MAXIMUM_PASSPHRASE_NUM_WORDS: u8 = 20; + +/// Represents a set of valid options to generate a passhprase with. +/// To get an instance of it, use [`PassphraseGeneratorRequest::validate_options`](PassphraseGeneratorRequest::validate_options) +struct ValidPassphraseGeneratorOptions { + pub(super) num_words: u8, + pub(super) word_separator: String, + pub(super) capitalize: bool, + pub(super) include_number: bool, +} + +impl PassphraseGeneratorRequest { + /// Validates the request and returns an immutable struct with valid options to use with the passphrase generator. + fn validate_options(self) -> Result { + // TODO: Add password generator policy checks + + if !(MINIMUM_PASSPHRASE_NUM_WORDS..=MAXIMUM_PASSPHRASE_NUM_WORDS).contains(&self.num_words) + { + return Err(Error::Internal("'num_words' must be between 3 and 20")); + } + + if self.word_separator.chars().next().is_none() { + return Err(Error::Internal("'word_separator' cannot be empty")); + }; + + Ok(ValidPassphraseGeneratorOptions { + num_words: self.num_words, + word_separator: self.word_separator, + capitalize: self.capitalize, + include_number: self.include_number, + }) + } +} + +/// Implementation of the random passphrase generator. This is not accessible to the public API. +/// See [`ClientGenerator::passphrase`](crate::ClientGenerator::passphrase) for the API function. +/// +/// # Arguments: +/// * `options`: Valid parameters used to generate the passphrase. To create it, use +/// [`PassphraseGeneratorRequest::validate_options`](PassphraseGeneratorRequest::validate_options). +pub(super) fn passphrase(request: PassphraseGeneratorRequest) -> Result { + let options = request.validate_options()?; + Ok(passphrase_with_rng(rand::thread_rng(), options)) +} + +fn passphrase_with_rng(mut rng: impl RngCore, options: ValidPassphraseGeneratorOptions) -> String { + let mut passphrase_words = gen_words(&mut rng, options.num_words); + if options.include_number { + include_number_in_words(&mut rng, &mut passphrase_words); + } + if options.capitalize { + capitalize_words(&mut passphrase_words); + } + passphrase_words.join(&options.word_separator) +} + +fn gen_words(mut rng: impl RngCore, num_words: u8) -> Vec { + (0..num_words) + .map(|_| { + EFF_LONG_WORD_LIST + .choose(&mut rng) + .expect("slice is not empty") + .to_string() + }) + .collect() +} + +fn include_number_in_words(mut rng: impl RngCore, words: &mut [String]) { + let number_idx = rng.gen_range(0..words.len()); + words[number_idx].push_str(&rng.gen_range(0..=9).to_string()); +} + +fn capitalize_words(words: &mut [String]) { + words + .iter_mut() + .for_each(|w| *w = capitalize_first_letter(w)); +} + +fn capitalize_first_letter(s: &str) -> String { + // Unicode case conversion can change the length of the string, so we can't capitalize in place. + // Instead we extract the first character and convert it to uppercase. This returns + // an iterator which we collect into a string, and then append the rest of the input. + let mut c = s.chars(); + match c.next() { + None => String::new(), + Some(f) => f.to_uppercase().collect::() + c.as_str(), + } +} + +#[cfg(test)] +mod tests { + use rand::SeedableRng; + + use super::*; + + #[test] + fn test_gen_words() { + let mut rng = rand_chacha::ChaCha8Rng::from_seed([0u8; 32]); + assert_eq!( + &gen_words(&mut rng, 4), + &["subsystem", "undertook", "silenced", "dinginess"] + ); + assert_eq!(&gen_words(&mut rng, 1), &["numbing"]); + assert_eq!(&gen_words(&mut rng, 2), &["catnip", "jokester"]); + } + + #[test] + fn test_capitalize() { + assert_eq!(capitalize_first_letter("hello"), "Hello"); + assert_eq!(capitalize_first_letter("1ello"), "1ello"); + assert_eq!(capitalize_first_letter("Hello"), "Hello"); + assert_eq!(capitalize_first_letter("h"), "H"); + assert_eq!(capitalize_first_letter(""), ""); + + // Also supports non-ascii, though the EFF list doesn't have any + assert_eq!(capitalize_first_letter("áéíóú"), "Áéíóú"); + } + + #[test] + fn test_capitalize_words() { + let mut words = vec!["hello".into(), "world".into()]; + capitalize_words(&mut words); + assert_eq!(words, &["Hello", "World"]); + } + + #[test] + fn test_include_number() { + let mut rng = rand_chacha::ChaCha8Rng::from_seed([0u8; 32]); + + let mut words = vec!["hello".into(), "world".into()]; + include_number_in_words(&mut rng, &mut words); + assert_eq!(words, &["hello", "world7"]); + + let mut words = vec!["This".into(), "is".into(), "a".into(), "test".into()]; + include_number_in_words(&mut rng, &mut words); + assert_eq!(words, &["This", "is", "a1", "test"]); + } + + #[test] + fn test_separator() { + let mut rng = rand_chacha::ChaCha8Rng::from_seed([0u8; 32]); + + let input = PassphraseGeneratorRequest { + num_words: 4, + word_separator: "👨🏻‍❤️‍💋‍👨🏻".into(), // This emoji is 35 bytes long, but represented as a single character + capitalize: false, + include_number: true, + } + .validate_options() + .unwrap(); + assert_eq!( + passphrase_with_rng(&mut rng, input), + "subsystem4👨🏻‍❤️‍💋‍👨🏻undertook👨🏻‍❤️‍💋‍👨🏻silenced👨🏻‍❤️‍💋‍👨🏻dinginess" + ); + } + + #[test] + fn test_passphrase() { + let mut rng = rand_chacha::ChaCha8Rng::from_seed([0u8; 32]); + + let input = PassphraseGeneratorRequest { + num_words: 4, + word_separator: "-".into(), + capitalize: true, + include_number: true, + } + .validate_options() + .unwrap(); + assert_eq!( + passphrase_with_rng(&mut rng, input), + "Subsystem4-Undertook-Silenced-Dinginess" + ); + + let input = PassphraseGeneratorRequest { + num_words: 3, + word_separator: " ".into(), + capitalize: false, + include_number: true, + } + .validate_options() + .unwrap(); + assert_eq!( + passphrase_with_rng(&mut rng, input), + "drew7 hankering cabana" + ); + + let input = PassphraseGeneratorRequest { + num_words: 5, + word_separator: ";".into(), + capitalize: false, + include_number: false, + } + .validate_options() + .unwrap(); + assert_eq!( + passphrase_with_rng(&mut rng, input), + "duller;backlight;factual;husked;remover" + ); + } +} diff --git a/crates/bitwarden/src/tool/generators/password.rs b/crates/bitwarden/src/tool/generators/password.rs index 0a3874082..cf986495c 100644 --- a/crates/bitwarden/src/tool/generators/password.rs +++ b/crates/bitwarden/src/tool/generators/password.rs @@ -1,49 +1,394 @@ -use crate::error::Result; +use std::collections::BTreeSet; + +use rand::{distributions::Distribution, seq::SliceRandom, RngCore}; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; -/// Password generator request. If all options are false, the default is to -/// generate a password with: -/// - lowercase -/// - uppercase -/// - numbers -/// -/// The default length is 16. -#[derive(Serialize, Deserialize, Debug, JsonSchema, Default)] +use crate::error::{Error, Result}; + +/// Password generator request options. +#[derive(Serialize, Deserialize, Debug, JsonSchema)] #[serde(rename_all = "camelCase", deny_unknown_fields)] #[cfg_attr(feature = "mobile", derive(uniffi::Record))] pub struct PasswordGeneratorRequest { + /// Include lowercase characters (a-z). pub lowercase: bool, + /// Include uppercase characters (A-Z). pub uppercase: bool, + /// Include numbers (0-9). pub numbers: bool, + /// Include special characters: ! @ # $ % ^ & * pub special: bool, - pub length: Option, + /// The length of the generated password. + /// Note that the password length must be greater than the sum of all the minimums. + pub length: u8, - pub avoid_ambiguous: Option, // TODO: Should we rename this to include_all_characters? - pub min_lowercase: Option, - pub min_uppercase: Option, - pub min_number: Option, - pub min_special: Option, + /// When set to true, the generated password will not contain ambiguous characters. + /// The ambiguous characters are: I, O, l, 0, 1 + pub avoid_ambiguous: bool, // TODO: Should we rename this to include_all_characters? + + /// The minimum number of lowercase characters in the generated password. + /// When set, the value must be between 1 and 9. This value is ignored is lowercase is false + pub min_lowercase: Option, + /// The minimum number of uppercase characters in the generated password. + /// When set, the value must be between 1 and 9. This value is ignored is uppercase is false + pub min_uppercase: Option, + /// The minimum number of numbers in the generated password. + /// When set, the value must be between 1 and 9. This value is ignored is numbers is false + pub min_number: Option, + /// The minimum number of special characters in the generated password. + /// When set, the value must be between 1 and 9. This value is ignored is special is false + pub min_special: Option, } -/// Passphrase generator request. -/// -/// The default separator is `-` and default number of words is 3. -#[derive(Serialize, Deserialize, Debug, JsonSchema, Default)] -#[serde(rename_all = "camelCase", deny_unknown_fields)] -#[cfg_attr(feature = "mobile", derive(uniffi::Record))] -pub struct PassphraseGeneratorRequest { - pub num_words: Option, - pub word_separator: Option, - pub capitalize: Option, - pub include_number: Option, +const DEFAULT_PASSWORD_LENGTH: u8 = 16; + +impl Default for PasswordGeneratorRequest { + fn default() -> Self { + Self { + lowercase: true, + uppercase: true, + numbers: true, + special: false, + length: DEFAULT_PASSWORD_LENGTH, + avoid_ambiguous: false, + min_lowercase: None, + min_uppercase: None, + min_number: None, + min_special: None, + } + } +} + +const UPPER_CHARS_AMBIGUOUS: &[char] = &['I', 'O']; +const LOWER_CHARS_AMBIGUOUS: &[char] = &['l']; +const NUMBER_CHARS_AMBIGUOUS: &[char] = &['0', '1']; +const SPECIAL_CHARS: &[char] = &['!', '@', '#', '$', '%', '^', '&', '*']; + +/// A set of characters used to generate a password. This set is backed by a BTreeSet +/// to have consistent ordering between runs. This is not important during normal execution, +/// but it's necessary for the tests to be repeatable. +/// To create an instance, use [`CharSet::default()`](CharSet::default) +#[derive(Clone, Default)] +struct CharSet(BTreeSet); +impl CharSet { + /// Includes the given characters in the set. Any duplicate items will be ignored + pub fn include(self, other: impl IntoIterator) -> Self { + self.include_if(true, other) + } + + /// Includes the given characters in the set if the predicate is true. Any duplicate items will be ignored + pub fn include_if(mut self, predicate: bool, other: impl IntoIterator) -> Self { + if predicate { + self.0.extend(other); + } + self + } + + /// Excludes the given characters from the set. Any missing items will be ignored + pub fn exclude_if<'a>( + self, + predicate: bool, + other: impl IntoIterator, + ) -> Self { + if predicate { + let other: BTreeSet<_> = other.into_iter().copied().collect(); + Self(self.0.difference(&other).copied().collect()) + } else { + self + } + } +} +impl<'a> IntoIterator for &'a CharSet { + type Item = char; + type IntoIter = std::iter::Copied>; + fn into_iter(self) -> Self::IntoIter { + self.0.iter().copied() + } +} +impl Distribution for CharSet { + fn sample(&self, rng: &mut R) -> char { + let idx = rng.gen_range(0..self.0.len()); + *self.0.iter().nth(idx).expect("Valid index") + } +} + +/// Represents a set of valid options to generate a password with. +/// To get an instance of it, use [`PasswordGeneratorRequest::validate_options`](PasswordGeneratorRequest::validate_options) +struct PasswordGeneratorOptions { + pub(super) lower: (CharSet, usize), + pub(super) upper: (CharSet, usize), + pub(super) number: (CharSet, usize), + pub(super) special: (CharSet, usize), + pub(super) all: (CharSet, usize), + + pub(super) length: usize, +} + +impl PasswordGeneratorRequest { + /// Validates the request and returns an immutable struct with valid options to use with the password generator. + fn validate_options(self) -> Result { + // TODO: Add password generator policy checks + + // We always have to have at least one character set enabled + if !self.lowercase && !self.uppercase && !self.numbers && !self.special { + return Err(Error::Internal( + "At least one character set must be enabled", + )); + } + + if self.length < 4 { + return Err(Error::Internal( + "A password must be at least 4 characters long", + )); + } + + // Make sure the minimum values are zero when the character + // set is disabled, and at least one when it's enabled + fn get_minimum(min: Option, enabled: bool) -> usize { + if enabled { + usize::max(min.unwrap_or(1) as usize, 1) + } else { + 0 + } + } + + let length = self.length as usize; + let min_lowercase = get_minimum(self.min_lowercase, self.lowercase); + let min_uppercase = get_minimum(self.min_uppercase, self.uppercase); + let min_number = get_minimum(self.min_number, self.numbers); + let min_special = get_minimum(self.min_special, self.special); + + // Check that the minimum lengths aren't larger than the password length + let minimum_length = min_lowercase + min_uppercase + min_number + min_special; + if minimum_length > length { + return Err(Error::Internal( + "Password length can't be less than the sum of the minimums", + )); + } + + let lower = ( + CharSet::default() + .include_if(self.lowercase, 'a'..='z') + .exclude_if(self.avoid_ambiguous, LOWER_CHARS_AMBIGUOUS), + min_lowercase, + ); + + let upper = ( + CharSet::default() + .include_if(self.uppercase, 'A'..='Z') + .exclude_if(self.avoid_ambiguous, UPPER_CHARS_AMBIGUOUS), + min_uppercase, + ); + + let number = ( + CharSet::default() + .include_if(self.numbers, '0'..='9') + .exclude_if(self.avoid_ambiguous, NUMBER_CHARS_AMBIGUOUS), + min_number, + ); + + let special = ( + CharSet::default().include_if(self.special, SPECIAL_CHARS.iter().copied()), + min_special, + ); + + let all = ( + CharSet::default() + .include(&lower.0) + .include(&upper.0) + .include(&number.0) + .include(&special.0), + length - minimum_length, + ); + + Ok(PasswordGeneratorOptions { + lower, + upper, + number, + special, + all, + length, + }) + } +} + +/// Implementation of the random password generator. This is not accessible to the public API. +/// See [`ClientGenerator::password`](crate::ClientGenerator::password) for the API function. +pub(super) fn password(input: PasswordGeneratorRequest) -> Result { + let options = input.validate_options()?; + Ok(password_with_rng(rand::thread_rng(), options)) } -pub(super) fn password(_input: PasswordGeneratorRequest) -> Result { - Ok("pa11w0rd".to_string()) +fn password_with_rng(mut rng: impl RngCore, options: PasswordGeneratorOptions) -> String { + let mut buf: Vec = Vec::with_capacity(options.length); + + let opts = [ + &options.all, + &options.upper, + &options.lower, + &options.number, + &options.special, + ]; + for (set, qty) in opts { + buf.extend(set.sample_iter(&mut rng).take(*qty)); + } + + buf.shuffle(&mut rng); + + buf.iter().collect() } -pub(super) fn passphrase(_input: PassphraseGeneratorRequest) -> Result { - Ok("correct-horse-battery-staple".to_string()) +#[cfg(test)] +mod test { + use std::collections::BTreeSet; + + use rand::SeedableRng; + + use super::*; + + // We convert the slices to BTreeSets to be able to use `is_subset` + fn ref_to_set<'a>(chars: impl IntoIterator) -> BTreeSet { + chars.into_iter().copied().collect() + } + fn to_set(chars: impl IntoIterator) -> BTreeSet { + chars.into_iter().collect() + } + + #[test] + fn test_password_gen_all_charsets_enabled() { + let mut rng = rand_chacha::ChaCha8Rng::from_seed([0u8; 32]); + + let options = PasswordGeneratorRequest { + lowercase: true, + uppercase: true, + numbers: true, + special: true, + avoid_ambiguous: false, + ..Default::default() + } + .validate_options() + .unwrap(); + + assert_eq!(to_set(&options.lower.0), to_set('a'..='z')); + assert_eq!(to_set(&options.upper.0), to_set('A'..='Z')); + assert_eq!(to_set(&options.number.0), to_set('0'..='9')); + assert_eq!(to_set(&options.special.0), ref_to_set(SPECIAL_CHARS)); + + let pass = password_with_rng(&mut rng, options); + assert_eq!(pass, "Z!^B5r%hUa23dFM@"); + } + + #[test] + fn test_password_gen_only_letters_enabled() { + let mut rng = rand_chacha::ChaCha8Rng::from_seed([0u8; 32]); + + let options = PasswordGeneratorRequest { + lowercase: true, + uppercase: true, + numbers: false, + special: false, + avoid_ambiguous: false, + ..Default::default() + } + .validate_options() + .unwrap(); + + assert_eq!(to_set(&options.lower.0), to_set('a'..='z')); + assert_eq!(to_set(&options.upper.0), to_set('A'..='Z')); + assert_eq!(to_set(&options.number.0), to_set([])); + assert_eq!(to_set(&options.special.0), to_set([])); + + let pass = password_with_rng(&mut rng, options); + assert_eq!(pass, "NQiFrGufQMiNUAmj"); + } + + #[test] + fn test_password_gen_only_numbers_and_lower_enabled_no_ambiguous() { + let mut rng = rand_chacha::ChaCha8Rng::from_seed([0u8; 32]); + + let options = PasswordGeneratorRequest { + lowercase: true, + uppercase: false, + numbers: true, + special: false, + avoid_ambiguous: true, + ..Default::default() + } + .validate_options() + .unwrap(); + + assert!(to_set(&options.lower.0).is_subset(&to_set('a'..='z'))); + assert!(to_set(&options.lower.0).is_disjoint(&ref_to_set(LOWER_CHARS_AMBIGUOUS))); + + assert!(to_set(&options.number.0).is_subset(&to_set('0'..='9'))); + assert!(to_set(&options.number.0).is_disjoint(&ref_to_set(NUMBER_CHARS_AMBIGUOUS))); + + assert_eq!(to_set(&options.upper.0), to_set([])); + assert_eq!(to_set(&options.special.0), to_set([])); + + let pass = password_with_rng(&mut rng, options); + assert_eq!(pass, "mnjabfz5ct272prf"); + } + + #[test] + fn test_password_gen_only_upper_and_special_enabled_no_ambiguous() { + let mut rng = rand_chacha::ChaCha8Rng::from_seed([0u8; 32]); + + let options = PasswordGeneratorRequest { + lowercase: false, + uppercase: true, + numbers: false, + special: true, + avoid_ambiguous: true, + ..Default::default() + } + .validate_options() + .unwrap(); + + assert!(to_set(&options.upper.0).is_subset(&to_set('A'..='Z'))); + assert!(to_set(&options.upper.0).is_disjoint(&ref_to_set(UPPER_CHARS_AMBIGUOUS))); + + assert_eq!(to_set(&options.special.0), ref_to_set(SPECIAL_CHARS)); + + assert_eq!(to_set(&options.lower.0), to_set([])); + assert_eq!(to_set(&options.number.0), to_set([])); + + let pass = password_with_rng(&mut rng, options); + assert_eq!(pass, "B*GBQANS%UZPQD!K"); + } + + #[test] + fn test_password_gen_minimum_limits() { + let mut rng = rand_chacha::ChaCha8Rng::from_seed([0u8; 32]); + + let options = PasswordGeneratorRequest { + lowercase: true, + uppercase: true, + numbers: true, + special: true, + avoid_ambiguous: false, + length: 24, + min_lowercase: Some(5), + min_uppercase: Some(5), + min_number: Some(5), + min_special: Some(5), + } + .validate_options() + .unwrap(); + + assert_eq!(to_set(&options.lower.0), to_set('a'..='z')); + assert_eq!(to_set(&options.upper.0), to_set('A'..='Z')); + assert_eq!(to_set(&options.number.0), to_set('0'..='9')); + assert_eq!(to_set(&options.special.0), ref_to_set(SPECIAL_CHARS)); + + assert_eq!(options.lower.1, 5); + assert_eq!(options.upper.1, 5); + assert_eq!(options.number.1, 5); + assert_eq!(options.special.1, 5); + + let pass = password_with_rng(&mut rng, options); + assert_eq!(pass, "236q5!a#R%PG5rI%k1!*@uRt"); + } } diff --git a/crates/bitwarden/src/vault/mod.rs b/crates/bitwarden/src/vault/mod.rs index 12910283c..ca9b12c1c 100644 --- a/crates/bitwarden/src/vault/mod.rs +++ b/crates/bitwarden/src/vault/mod.rs @@ -10,4 +10,5 @@ pub use collection::{Collection, CollectionView}; pub use folder::{Folder, FolderView}; pub use password_history::{PasswordHistory, PasswordHistoryView}; pub use send::{Send, SendListView, SendView}; -pub use totp::{generate_totp, TotpResponse}; +pub(crate) use totp::generate_totp; +pub use totp::TotpResponse; diff --git a/crates/bitwarden/src/vault/totp.rs b/crates/bitwarden/src/vault/totp.rs index 7e701f92e..e080a85a9 100644 --- a/crates/bitwarden/src/vault/totp.rs +++ b/crates/bitwarden/src/vault/totp.rs @@ -1,7 +1,23 @@ +use std::{collections::HashMap, str::FromStr}; + +use crate::error::{Error, Result}; use chrono::{DateTime, Utc}; +use data_encoding::BASE32; +use hmac::{Hmac, Mac}; +use reqwest::Url; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; +type HmacSha1 = Hmac; +type HmacSha256 = Hmac; +type HmacSha512 = Hmac; + +const STEAM_CHARS: &str = "23456789BCDFGHJKMNPQRTVWXY"; + +const DEFAULT_ALGORITHM: Algorithm = Algorithm::Sha1; +const DEFAULT_DIGITS: u32 = 6; +const DEFAULT_PERIOD: u32 = 30; + #[derive(Serialize, Deserialize, Debug, JsonSchema)] #[serde(rename_all = "camelCase", deny_unknown_fields)] #[cfg_attr(feature = "mobile", derive(uniffi::Record))] @@ -22,9 +38,237 @@ pub struct TotpResponse { /// - Steam URI /// /// Supports providing an optional time, and defaults to current system time if none is provided. -pub async fn generate_totp(_key: String, _time: Option>) -> TotpResponse { - TotpResponse { - code: "000 000".to_string(), - period: 30, +/// +/// Arguments: +/// - `key` - The key to generate the TOTP code from +/// - `time` - The time in UTC to generate the TOTP code for, defaults to current system time +pub(crate) fn generate_totp(key: String, time: Option>) -> Result { + let params: Totp = key.parse()?; + + let time = time.unwrap_or_else(Utc::now); + + let otp = params.derive_otp(time.timestamp()); + + Ok(TotpResponse { + code: otp, + period: params.period, + }) +} + +#[derive(Clone, Copy, Debug)] +enum Algorithm { + Sha1, + Sha256, + Sha512, + Steam, +} + +impl Algorithm { + // Derive the HMAC hash for the given algorithm + fn derive_hash(&self, key: &[u8], time: &[u8]) -> Vec { + fn compute_digest(digest: D, time: &[u8]) -> Vec { + digest.chain_update(time).finalize().into_bytes().to_vec() + } + + match self { + Algorithm::Sha1 => compute_digest( + HmacSha1::new_from_slice(key).expect("hmac new_from_slice should not fail"), + time, + ), + Algorithm::Sha256 => compute_digest( + HmacSha256::new_from_slice(key).expect("hmac new_from_slice should not fail"), + time, + ), + Algorithm::Sha512 => compute_digest( + HmacSha512::new_from_slice(key).expect("hmac new_from_slice should not fail"), + time, + ), + Algorithm::Steam => compute_digest( + HmacSha1::new_from_slice(key).expect("hmac new_from_slice should not fail"), + time, + ), + } + } +} + +#[derive(Debug)] +struct Totp { + algorithm: Algorithm, + digits: u32, + period: u32, + secret: Vec, +} + +impl Totp { + fn derive_otp(&self, time: i64) -> String { + let time = time / self.period as i64; + + let hash = self + .algorithm + .derive_hash(&self.secret, time.to_be_bytes().as_ref()); + let binary = derive_binary(hash); + + if let Algorithm::Steam = self.algorithm { + derive_steam_otp(binary, self.digits) + } else { + let otp = binary % 10_u32.pow(self.digits); + format!("{1:00$}", self.digits as usize, otp) + } + } +} + +impl FromStr for Totp { + type Err = Error; + + /// Parses the provided key and returns the corresponding `Totp`. + /// + /// Key can be either: + /// - A base32 encoded string + /// - OTP Auth URI + /// - Steam URI + fn from_str(key: &str) -> Result { + fn decode_secret(secret: &str) -> Result> { + BASE32 + .decode(secret.as_bytes()) + .map_err(|_| Error::Internal("Unable to decode secret")) + } + + let params = if key.starts_with("otpauth://") { + let url = Url::parse(key).map_err(|_| Error::Internal("Unable to parse URL"))?; + let parts: HashMap<_, _> = url.query_pairs().collect(); + + Totp { + algorithm: parts + .get("algorithm") + .and_then(|v| match v.to_uppercase().as_ref() { + "SHA1" => Some(Algorithm::Sha1), + "SHA256" => Some(Algorithm::Sha256), + "SHA512" => Some(Algorithm::Sha512), + _ => None, + }) + .unwrap_or(DEFAULT_ALGORITHM), + digits: parts + .get("digits") + .and_then(|v| v.parse().ok()) + .map(|v: u32| v.clamp(0, 10)) + .unwrap_or(DEFAULT_DIGITS), + period: parts + .get("period") + .and_then(|v| v.parse().ok()) + .map(|v: u32| v.max(1)) + .unwrap_or(DEFAULT_PERIOD), + secret: decode_secret( + &parts + .get("secret") + .map(|v| v.to_string()) + .ok_or(Error::Internal("Missing secret in otpauth URI"))?, + )?, + } + } else if let Some(secret) = key.strip_prefix("steam://") { + Totp { + algorithm: Algorithm::Steam, + digits: 5, + period: DEFAULT_PERIOD, + secret: decode_secret(secret)?, + } + } else { + Totp { + algorithm: DEFAULT_ALGORITHM, + digits: DEFAULT_DIGITS, + period: DEFAULT_PERIOD, + secret: decode_secret(key)?, + } + }; + + Ok(params) + } +} + +/// Derive the Steam OTP from the hash with the given number of digits. +fn derive_steam_otp(binary: u32, digits: u32) -> String { + let mut full_code = binary & 0x7fffffff; + + (0..digits) + .map(|_| { + let index = full_code as usize % STEAM_CHARS.len(); + let char = STEAM_CHARS + .chars() + .nth(index) + .expect("Should always be within range"); + full_code /= STEAM_CHARS.len() as u32; + char + }) + .collect() +} + +/// Derive the OTP from the hash with the given number of digits. +fn derive_binary(hash: Vec) -> u32 { + let offset = (hash.last().unwrap_or(&0) & 15) as usize; + + ((hash[offset] & 127) as u32) << 24 + | (hash[offset + 1] as u32) << 16 + | (hash[offset + 2] as u32) << 8 + | hash[offset + 3] as u32 +} + +#[cfg(test)] +mod tests { + use super::*; + use chrono::Utc; + + #[test] + fn test_generate_totp() { + let key = "WQIQ25BRKZYCJVYP".to_string(); + let time = Some( + DateTime::parse_from_rfc3339("2023-01-01T00:00:00.000Z") + .unwrap() + .with_timezone(&Utc), + ); + let response = generate_totp(key, time).unwrap(); + + assert_eq!(response.code, "194506".to_string()); + assert_eq!(response.period, 30); + } + + #[test] + fn test_generate_otpauth() { + let key = "otpauth://totp/test-account?secret=WQIQ25BRKZYCJVYP".to_string(); + let time = Some( + DateTime::parse_from_rfc3339("2023-01-01T00:00:00.000Z") + .unwrap() + .with_timezone(&Utc), + ); + let response = generate_totp(key, time).unwrap(); + + assert_eq!(response.code, "194506".to_string()); + assert_eq!(response.period, 30); + } + + #[test] + fn test_generate_otpauth_period() { + let key = "otpauth://totp/test-account?secret=WQIQ25BRKZYCJVYP&period=60".to_string(); + let time = Some( + DateTime::parse_from_rfc3339("2023-01-01T00:00:00.000Z") + .unwrap() + .with_timezone(&Utc), + ); + let response = generate_totp(key, time).unwrap(); + + assert_eq!(response.code, "730364".to_string()); + assert_eq!(response.period, 60); + } + + #[test] + fn test_generate_steam() { + let key = "steam://HXDMVJECJJWSRB3HWIZR4IFUGFTMXBOZ".to_string(); + let time = Some( + DateTime::parse_from_rfc3339("2023-01-01T00:00:00.000Z") + .unwrap() + .with_timezone(&Utc), + ); + let response = generate_totp(key, time).unwrap(); + + assert_eq!(response.code, "7W6CJ".to_string()); + assert_eq!(response.period, 30); } } diff --git a/crates/bw/Cargo.toml b/crates/bw/Cargo.toml index 9ceb2b153..706628f53 100644 --- a/crates/bw/Cargo.toml +++ b/crates/bw/Cargo.toml @@ -13,7 +13,7 @@ Bitwarden Password Manager CLI keywords = ["bitwarden", "password-manager", "cli"] [dependencies] -clap = { version = "4.4.8", features = ["derive", "env"] } +clap = { version = "4.4.10", features = ["derive", "env"] } color-eyre = "0.6" env_logger = "0.10.1" inquire = "0.6.2" diff --git a/crates/bw/src/main.rs b/crates/bw/src/main.rs index 609650ab1..236aef22d 100644 --- a/crates/bw/src/main.rs +++ b/crates/bw/src/main.rs @@ -1,5 +1,7 @@ use bitwarden::{ - auth::RegisterRequest, client::client_settings::ClientSettings, tool::PasswordGeneratorRequest, + auth::RegisterRequest, + client::client_settings::ClientSettings, + tool::{PassphraseGeneratorRequest, PasswordGeneratorRequest}, }; use bitwarden_cli::{install_color_eyre, text_prompt_when_none, Color}; use clap::{command, Args, CommandFactory, Parser, Subcommand}; @@ -87,7 +89,7 @@ enum ItemCommands { #[derive(Subcommand, Clone)] enum GeneratorCommands { Password(PasswordGeneratorArgs), - Passphrase {}, + Passphrase(PassphraseGeneratorArgs), } #[derive(Args, Clone)] @@ -113,6 +115,18 @@ struct PasswordGeneratorArgs { length: u8, } +#[derive(Args, Clone)] +struct PassphraseGeneratorArgs { + #[arg(long, default_value = "3", help = "Number of words in the passphrase")] + words: u8, + #[arg(long, default_value = " ", help = "Separator between words")] + separator: char, + #[arg(long, action, help = "Capitalize the first letter of each word")] + capitalize: bool, + #[arg(long, action, help = "Include a number in one of the words")] + include_number: bool, +} + #[tokio::main(flavor = "current_thread")] async fn main() -> Result<()> { env_logger::Builder::from_env(env_logger::Env::default().default_filter_or("info")).init(); @@ -199,14 +213,26 @@ async fn process_commands() -> Result<()> { uppercase: args.uppercase, numbers: args.numbers, special: args.special, - length: Some(args.length), + length: args.length, ..Default::default() }) .await?; println!("{}", password); } - GeneratorCommands::Passphrase {} => todo!(), + GeneratorCommands::Passphrase(args) => { + let passphrase = client + .generator() + .passphrase(PassphraseGeneratorRequest { + num_words: args.words, + word_separator: args.separator.to_string(), + capitalize: args.capitalize, + include_number: args.include_number, + }) + .await?; + + println!("{}", passphrase); + } }, }; diff --git a/crates/bws/Cargo.toml b/crates/bws/Cargo.toml index 95dbef003..cef520b1a 100644 --- a/crates/bws/Cargo.toml +++ b/crates/bws/Cargo.toml @@ -20,7 +20,7 @@ chrono = { version = "0.4.31", features = [ "clock", "std", ], default-features = false } -clap = { version = "4.4.8", features = ["derive", "env", "string"] } +clap = { version = "4.4.10", features = ["derive", "env", "string"] } clap_complete = "4.4.4" color-eyre = "0.6" comfy-table = "^7.1.0" diff --git a/crates/sdk-schemas/src/main.rs b/crates/sdk-schemas/src/main.rs index 9d7e52a47..0cfc045a0 100644 --- a/crates/sdk-schemas/src/main.rs +++ b/crates/sdk-schemas/src/main.rs @@ -1,8 +1,7 @@ use std::{fs::File, io::Write}; use anyhow::Result; -use itertools::Itertools; -use schemars::{schema::RootSchema, schema_for}; +use schemars::{schema::RootSchema, schema_for, JsonSchema}; /// Creates a json schema file for any type passed in using Schemars. The filename and path of the generated /// schema file is derived from the namespace passed into the macro or supplied as the first argument. @@ -45,6 +44,8 @@ use schemars::{schema::RootSchema, schema_for}; /// will generate `Response.json` at `{{pwd}}/path/to/folder/Response.json` macro_rules! write_schema_for { ($type:ty) => { + use itertools::Itertools; + let schema = schema_for!($type); let type_name = stringify!($type); @@ -65,12 +66,6 @@ macro_rules! write_schema_for { }; } -macro_rules! write_schema_for_response { - ( $($type:ty),+ $(,)? ) => { - $( write_schema_for!("response", bitwarden_json::response::Response<$type>); )+ - }; -} - fn write_schema(schema: RootSchema, dir_path: String, type_name: String) -> Result<()> { let file_name = type_name .split("::") @@ -88,33 +83,39 @@ fn write_schema(schema: RootSchema, dir_path: String, type_name: String) -> Resu Ok(()) } -fn main() -> Result<()> { +use bitwarden_json::response::Response; + +#[allow(dead_code)] +#[derive(JsonSchema)] +struct SchemaTypes { // Input types for new Client - write_schema_for!(bitwarden::client::client_settings::ClientSettings); + client_settings: bitwarden::client::client_settings::ClientSettings, + // Input types for Client::run_command - write_schema_for!(bitwarden_json::command::Command); + input_command: bitwarden_json::command::Command, // Output types for Client::run_command - // Only add structs which are direct results of SDK commands. - write_schema_for_response! { - bitwarden::auth::login::ApiKeyLoginResponse, - bitwarden::auth::login::PasswordLoginResponse, - bitwarden::secrets_manager::secrets::SecretIdentifiersResponse, - bitwarden::secrets_manager::secrets::SecretResponse, - bitwarden::secrets_manager::secrets::SecretsResponse, - bitwarden::secrets_manager::secrets::SecretsDeleteResponse, - bitwarden::secrets_manager::projects::ProjectResponse, - bitwarden::secrets_manager::projects::ProjectsResponse, - bitwarden::secrets_manager::projects::ProjectsDeleteResponse, - }; + api_key_login: Response, + password_login: Response, + access_token_login: Response, + secret_identifiers: Response, + secret: Response, + secrets: Response, + secrets_delete: Response, + project: Response, + projects: Response, + projects_delete: Response, - // Same as above, but for the internal feature #[cfg(feature = "internal")] - write_schema_for_response! { - bitwarden::platform::FingerprintResponse, - bitwarden::platform::SyncResponse, - bitwarden::platform::UserApiKeyResponse, - }; + fingerprint: Response, + #[cfg(feature = "internal")] + sync: Response, + #[cfg(feature = "internal")] + user_api_key: Response, +} + +fn main() -> Result<()> { + write_schema_for!("schema_types", SchemaTypes); #[cfg(feature = "internal")] write_schema_for!(bitwarden_uniffi::docs::DocRef); diff --git a/crates/uniffi-bindgen/Cargo.toml b/crates/uniffi-bindgen/Cargo.toml index 825f923e3..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.25.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/Bitwarden.Sdk/Bitwarden.Sdk.csproj b/languages/csharp/Bitwarden.Sdk/Bitwarden.Sdk.csproj index c110785a9..951b893ab 100644 --- a/languages/csharp/Bitwarden.Sdk/Bitwarden.Sdk.csproj +++ b/languages/csharp/Bitwarden.Sdk/Bitwarden.Sdk.csproj @@ -24,7 +24,7 @@ - + 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/kotlin/doc.md b/languages/kotlin/doc.md index fd65c7b71..a84397719 100644 --- a/languages/kotlin/doc.md +++ b/languages/kotlin/doc.md @@ -327,6 +327,19 @@ Decrypt password history **Output**: std::result::Result +## ClientPlatform + +### `fingerprint` + +Fingerprint + +**Arguments**: + +- self: +- req: [FingerprintRequest](#fingerprintrequest) + +**Output**: std::result::Result + ## ClientSends ### `encrypt` @@ -818,6 +831,26 @@ implementations. +## `FingerprintRequest` + + + + + + + + + + + + + + + + + +
KeyTypeDescription
fingerprintMaterialstringThe input material, used in the fingerprint generation process.
publicKeystringThe user's public key encoded with base64.
+ ## `Folder` @@ -1087,23 +1120,23 @@ implementations. - - + + - - + + - - + + - - + +
numWordsinteger,nullintegerNumber of words in the generated passphrase. This value must be between 3 and 20.
wordSeparatorstring,nullstringCharacter separator between words in the generated passphrase. If the value is set, it cannot be empty.
capitalizeboolean,nullbooleanWhen set to true, capitalize the first letter of each word in the generated passphrase.
includeNumberboolean,nullbooleanWhen set to true, include a number at the end of one of the words in the generated passphrase.
diff --git a/languages/php/.gitignore b/languages/php/.gitignore new file mode 100644 index 000000000..b2a69e9a0 --- /dev/null +++ b/languages/php/.gitignore @@ -0,0 +1,2 @@ +.DS_Store +vendor diff --git a/languages/php/README.md b/languages/php/README.md new file mode 100644 index 000000000..9e4a9385d --- /dev/null +++ b/languages/php/README.md @@ -0,0 +1,100 @@ +# Bitwarden Secrets Manager SDK wrapper for PHP + +PHP bindings for interacting with the [Bitwarden Secrets Manager]. This is a beta release and might be missing some functionality. +Supported are CRUD operations on project and secret entities. + +## Installation + +Requirements: +- PHP >= 8.0 +- Composer +- Bitwarden C libraries which you can generate using BitwardenSDK and following instructions in its readme (requires Rust). https://github.com/bitwarden/sdk +If you are not using the standalone version of this library, file will be placed in `target/debug` folder if you are using from BitwardenSDK repository. +- Access token for the Bitwarden account + + +## Usage + +To interact with the client first you need to obtain the access token from Bitwarden. +You can then initialize BitwardenSettings passing $api_url and $identity_url if needed. These parameteres are +optional and if they are not defined, BitwardenSettings instance will try to get these values from ENV, and +if they are not defined there as well, it will use defaults: `https://api.bitwarden.com` as api_url and +`https://identity.bitwarden.com` as identity_url. You can also pass device type as argument but that is entirely +optional. + +Passing BitwardenSettings instance to BitwardenClient will initialize it. Before using the client you must +be authorized by calling the access_token_login method passing your Bitwarden access token to it. + + +```php +$access_token = ''; +$api_url = ""; +$identity_url = ""; +$bitwarden_settings = new \Bitwarden\Sdk\BitwardenSettings($api_url, $identity_url); + +$bitwarden_client = new \Bitwarden\Sdk\BitwardenClient($bitwarden_settings); +$bitwarden_client->access_token_login($access_token); +``` + +After successful authorization you can interact with client to manage your projects and secrets. +```php +$organization_id = ""; + +$bitwarden_client = new \Bitwarden\Sdk\BitwardenClient($bitwarden_settings); +$res = $bitwarden_client->access_token_login($access_token); + +// create project +$name = "PHP project" +$res = $bitwarden_client->projects->create($name, $organization_id); +$project_id = $res->id; + +// get project +$res = $bitwarden_client->projects->get($project_id); + +// list projects +$res = $bitwarden_client->projects->list($organization_id); + +// update project +$name = "Updated PHP project" +$res = $bitwarden_client->projects->put($project_id, $name, $organization_id); + +// get secret +$res = $bitwarden_client->secrets->get($secret_id); + +// list secrets +$res = $bitwarden_client->secrets->list($organization_id); + +// delete project +$res = $bitwarden_client->projects->delete([$project_id]); + +``` + +Similarly, you interact with secrets: +```php +$organization_id = ""; + +// create secret +$key = "AWS secret key"; +$note = "Private account"; +$secret = "76asaj,Is_)" +$res = $bitwarden_client->secrets->create($key, $note, $organization_id, [$project_id], $secret); +$secret_id = $res->id; + +// get secret +$res = $bitwarden_sdk->secrets->get($secret_id); + +// list secrets +$res = $bitwarden_client->secrets->list($organization_id); + +// update secret +$note = "Updated account"; +$key = "AWS private updated" +$secret = "7uYTE,:Aer" +$res = $bitwarden_client->secrets->update($secret_id, $key, $note, $organization_id, [$project_id], $secret); + +// delete secret +$res = $bitwarden_sdk->secrets->delete([$secret_id]); +``` + + +[Bitwarden Secrets Manager]: https://bitwarden.com/products/secrets-manager/ diff --git a/languages/php/composer.json b/languages/php/composer.json new file mode 100644 index 000000000..18b333eac --- /dev/null +++ b/languages/php/composer.json @@ -0,0 +1,22 @@ +{ + "name": "bitwarden/sdk", + "description": "PHP bindings for interacting with the Bitwarden Secrets Manager. This is a beta release and might be missing some functionality.", + "type": "library", + "keywords": ["bitwarden","sdk","password-manager"], + "homepage": "https://github.com/bitwarden/sdk", + "require": { + "php": "^8.0", + "swaggest/json-schema": "^0.12.42", + "ext-ffi": "*" + }, + "autoload": { + "psr-4": { + "Bitwarden\\Sdk\\": "src/" + } + }, + "authors": [ + { + "name": "Bitwarden Inc." + } + ] +} diff --git a/languages/php/composer.lock b/languages/php/composer.lock new file mode 100644 index 000000000..fc6b42c4f --- /dev/null +++ b/languages/php/composer.lock @@ -0,0 +1,247 @@ +{ + "_readme": [ + "This file locks the dependencies of your project to a known state", + "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", + "This file is @generated automatically" + ], + "content-hash": "7081b1bfe099982a63ad06d5ab9fa66d", + "packages": [ + { + "name": "phplang/scope-exit", + "version": "1.0.0", + "source": { + "type": "git", + "url": "https://github.com/phplang/scope-exit.git", + "reference": "239b73abe89f9414aa85a7ca075ec9445629192b" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phplang/scope-exit/zipball/239b73abe89f9414aa85a7ca075ec9445629192b", + "reference": "239b73abe89f9414aa85a7ca075ec9445629192b", + "shasum": "" + }, + "require-dev": { + "phpunit/phpunit": "*" + }, + "type": "library", + "autoload": { + "psr-4": { + "PhpLang\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD" + ], + "authors": [ + { + "name": "Sara Golemon", + "email": "pollita@php.net", + "homepage": "https://twitter.com/SaraMG", + "role": "Developer" + } + ], + "description": "Emulation of SCOPE_EXIT construct from C++", + "homepage": "https://github.com/phplang/scope-exit", + "keywords": [ + "cleanup", + "exit", + "scope" + ], + "support": { + "issues": "https://github.com/phplang/scope-exit/issues", + "source": "https://github.com/phplang/scope-exit/tree/master" + }, + "time": "2016-09-17T00:15:18+00:00" + }, + { + "name": "swaggest/json-diff", + "version": "v3.10.4", + "source": { + "type": "git", + "url": "https://github.com/swaggest/json-diff.git", + "reference": "f4e511708060ff7511a3743fab4aa484a062bcfb" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/swaggest/json-diff/zipball/f4e511708060ff7511a3743fab4aa484a062bcfb", + "reference": "f4e511708060ff7511a3743fab4aa484a062bcfb", + "shasum": "" + }, + "require": { + "ext-json": "*" + }, + "require-dev": { + "phperf/phpunit": "4.8.37" + }, + "type": "library", + "autoload": { + "psr-4": { + "Swaggest\\JsonDiff\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Viacheslav Poturaev", + "email": "vearutop@gmail.com" + } + ], + "description": "JSON diff/rearrange/patch/pointer library for PHP", + "support": { + "issues": "https://github.com/swaggest/json-diff/issues", + "source": "https://github.com/swaggest/json-diff/tree/v3.10.4" + }, + "time": "2022-11-09T13:21:05+00:00" + }, + { + "name": "swaggest/json-schema", + "version": "v0.12.42", + "source": { + "type": "git", + "url": "https://github.com/swaggest/php-json-schema.git", + "reference": "d23adb53808b8e2da36f75bc0188546e4cbe3b45" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/swaggest/php-json-schema/zipball/d23adb53808b8e2da36f75bc0188546e4cbe3b45", + "reference": "d23adb53808b8e2da36f75bc0188546e4cbe3b45", + "shasum": "" + }, + "require": { + "ext-json": "*", + "php": ">=5.4", + "phplang/scope-exit": "^1.0", + "swaggest/json-diff": "^3.8.2", + "symfony/polyfill-mbstring": "^1.19" + }, + "require-dev": { + "phperf/phpunit": "4.8.37" + }, + "suggest": { + "ext-mbstring": "For better performance" + }, + "type": "library", + "autoload": { + "psr-4": { + "Swaggest\\JsonSchema\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Viacheslav Poturaev", + "email": "vearutop@gmail.com" + } + ], + "description": "High definition PHP structures with JSON-schema based validation", + "support": { + "email": "vearutop@gmail.com", + "issues": "https://github.com/swaggest/php-json-schema/issues", + "source": "https://github.com/swaggest/php-json-schema/tree/v0.12.42" + }, + "time": "2023-09-12T14:43:42+00:00" + }, + { + "name": "symfony/polyfill-mbstring", + "version": "v1.28.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-mbstring.git", + "reference": "42292d99c55abe617799667f454222c54c60e229" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/42292d99c55abe617799667f454222c54c60e229", + "reference": "42292d99c55abe617799667f454222c54c60e229", + "shasum": "" + }, + "require": { + "php": ">=7.1" + }, + "provide": { + "ext-mbstring": "*" + }, + "suggest": { + "ext-mbstring": "For best performance" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "1.28-dev" + }, + "thanks": { + "name": "symfony/polyfill", + "url": "https://github.com/symfony/polyfill" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Mbstring\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill for the Mbstring extension", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "mbstring", + "polyfill", + "portable", + "shim" + ], + "support": { + "source": "https://github.com/symfony/polyfill-mbstring/tree/v1.28.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2023-07-28T09:04:16+00:00" + } + ], + "packages-dev": [], + "aliases": [], + "minimum-stability": "stable", + "stability-flags": [], + "prefer-stable": false, + "prefer-lowest": false, + "platform": { + "php": "^8.0", + "ext-ffi": "*" + }, + "platform-dev": [], + "plugin-api-version": "2.6.0" +} diff --git a/languages/php/example.php b/languages/php/example.php new file mode 100644 index 000000000..0fdb6930a --- /dev/null +++ b/languages/php/example.php @@ -0,0 +1,43 @@ +'; +$organization_id = ""; + +$client_settings = new \Bitwarden\Sdk\BitwardenSettings(); + +$bitwarden_client = new \Bitwarden\Sdk\BitwardenClient($client_settings); +$bitwarden_client->access_token_login($access_token); + +// create project +$res = $bitwarden_client->projects->create('php project', $organization_id); +$project_id = $res->id; + +// get project +$res = $bitwarden_client->projects->get($project_id); + +// list projects +$res = $bitwarden_client->projects->list($organization_id); + +// update project +$res = $bitwarden_client->projects->put($project_id, 'php test awesome', $organization_id); + +// create secret +$res = $bitwarden_client->secrets->create("New Key", "hello world", $organization_id, [$project_id], "123"); +$secret_id = $res->id; + +// get secret +$res = $bitwarden_client->secrets->get($secret_id); + +// list secrets +$res = $bitwarden_client->secrets->list($organization_id); + +// update secret +$res = $bitwarden_client->secrets->update($secret_id, "hello world 2", "hello", $organization_id, [$project_id], "123"); + +// delete secret +$res = $bitwarden_client->secrets->delete([$secret_id]); + +// delete project +$res = $bitwarden_client->projects->delete([$project_id]); diff --git a/languages/php/src/BitwardenClient.php b/languages/php/src/BitwardenClient.php new file mode 100644 index 000000000..79fccdf9c --- /dev/null +++ b/languages/php/src/BitwardenClient.php @@ -0,0 +1,64 @@ +clientSettings = new ClientSettings(); + $this->clientSettings->apiUrl = $bitwardenSettings->get_api_url(); + $this->clientSettings->identityUrl = $bitwardenSettings->get_identity_url(); + $this->clientSettings->userAgent = "Bitwarden PHP-SDK"; + + $this->bitwarden_lib = new BitwardenLib(); + $this->handle = $this->bitwarden_lib->init($this->clientSettings); + + $this->commandRunner = new CommandRunner($this->bitwarden_lib, $this->handle); + $this->projects = new ProjectsClient($this->commandRunner); + $this->secrets = new SecretsClient($this->commandRunner); + } + + /** + * @throws \Exception + */ + public function access_token_login(string $access_token) + { + $access_token_request = new AccessTokenLoginRequest(); + $access_token_request->accessToken = $access_token; + $command = new Command(); + $command->accessTokenLogin = $access_token_request->jsonSerialize(); + $result = $this->commandRunner->run($command); + if (!isset($result->authenticated)) { + throw new \Exception("Authorization error"); + } + + if ($result->authenticated == False) { + throw new \Exception("Unauthorized"); + } + } + + public function __destruct() + { + $this->bitwarden_lib->free_mem(); + } +} diff --git a/languages/php/src/BitwardenLib.php b/languages/php/src/BitwardenLib.php new file mode 100644 index 000000000..3eb3ed5f4 --- /dev/null +++ b/languages/php/src/BitwardenLib.php @@ -0,0 +1,79 @@ +ffi = FFI::cdef(' + void* init(const char* param); + char* run_command(void* c_str_ptr, void* client_ptr); + void free_mem(void* client_ptr);', + $lib_file + ); + } + + public function init(ClientSettings $client_settings): FFI\CData + { + $this->handle = $this->ffi->init(json_encode($client_settings->jsonSerialize())); + return $this->handle; + } + + public function run_command(Command $command): \stdClass + { + $encoded_json = json_encode($command->jsonSerialize()); + try { + $result = $this->ffi->run_command($encoded_json, $this->handle); + return json_decode(FFI::string($result)); + } catch (\FFI\Exception $e) { + throw new \RuntimeException('Error occurred during FFI operation: ' . $e->getMessage()); + } + } + + public function free_mem(): void + { + $this->ffi->free_mem($this->handle); + } +} diff --git a/languages/php/src/BitwardenSettings.php b/languages/php/src/BitwardenSettings.php new file mode 100644 index 000000000..b3d62bc2e --- /dev/null +++ b/languages/php/src/BitwardenSettings.php @@ -0,0 +1,26 @@ +api_url = $api_url; + $this->identity_url = $identity_url; + } + + public function get_api_url(): ?string + { + return $this->api_url; + } + + public function get_identity_url(): ?string + { + return $this->identity_url; + } +} diff --git a/languages/php/src/CommandRunner.php b/languages/php/src/CommandRunner.php new file mode 100644 index 000000000..9eec68b2d --- /dev/null +++ b/languages/php/src/CommandRunner.php @@ -0,0 +1,37 @@ +bitwardenLib = $bitwardenLib; + $this->handle = $handle; + } + + /** + * @throws \Exception + */ + public function run(Command $command): \stdClass + { + $result = $this->bitwardenLib->run_command($command); + if ($result->success == true) { + return $result->data; + } + + if (isset($result->errorMessage)) + { + throw new \Exception($result->errorMessage); + } + throw new \Exception("Unknown error occurred"); + } +} diff --git a/languages/php/src/ProjectsClient.php b/languages/php/src/ProjectsClient.php new file mode 100644 index 000000000..6b6f9fb6a --- /dev/null +++ b/languages/php/src/ProjectsClient.php @@ -0,0 +1,81 @@ +commandRunner = $commandRunner; + } + + public function get(string $project_id): \stdClass + { + $project_get_request = new ProjectGetRequest(); + $project_get_request->id = $project_id; + $project_get_request->validate(); + $project_command = new ProjectsCommand(); + $project_command->get = $project_get_request->jsonSerialize(); + return $this->run_project_command($project_command); + } + + public function list(string $organization_id): \stdClass + { + $project_list_request = new ProjectsListRequest(); + $project_list_request->organizationId = $organization_id; + $project_list_request->validate(); + $project_command = new ProjectsCommand(); + $project_command->list = $project_list_request->jsonSerialize(); + return $this->run_project_command($project_command); + } + + public function create(string $project_name, string $organization_id): \stdClass + { + $project_create_request = new ProjectCreateRequest(); + $project_create_request->name = $project_name; + $project_create_request->organizationId = $organization_id; + $project_create_request->validate(); + $project_command = new ProjectsCommand(); + $project_command->create = $project_create_request->jsonSerialize(); + return $this->run_project_command($project_command); + } + + public function put(string $project_id, string $project_name, string $organization_id): \stdClass + { + $project_put_request = new ProjectPutRequest(); + $project_put_request->organizationId = $organization_id; + $project_put_request->name = $project_name; + $project_put_request->id = $project_id; + $project_put_request->validate(); + $project_command = new ProjectsCommand(); + $project_command->update = $project_put_request->jsonSerialize(); + return $this->run_project_command($project_command); + } + + public function delete(array $ids): \stdClass + { + $projects_delete_request = new ProjectsDeleteRequest(); + $projects_delete_request->ids = $ids; + $projects_delete_request->validate(); + $project_command = new ProjectsCommand(); + $project_command->delete = $projects_delete_request->jsonSerialize(); + return $this->run_project_command($project_command); + } + + public function run_project_command($projectCommand): \stdClass + { + $command = new Command(); + $command->projects = $projectCommand; + return $this->commandRunner->run($command); + } +} diff --git a/languages/php/src/SecretsClient.php b/languages/php/src/SecretsClient.php new file mode 100644 index 000000000..d5c0b0cef --- /dev/null +++ b/languages/php/src/SecretsClient.php @@ -0,0 +1,98 @@ +commandRunner = $commandRunner; + } + + public function get(string $secret_id): \stdClass + { + $secret_get_request = new SecretGetRequest(); + $secret_get_request->id = $secret_id; + $secret_get_request->validate(); + $secret_command = new SecretsCommand(); + $secret_command->get = $secret_get_request->jsonSerialize(); + return $this->run_secret_command($secret_command); + } + + public function get_by_ids(array $secret_ids): \stdClass + { + $project_get_by_ids_request = new SecretsGetRequest(); + $project_get_by_ids_request->ids = $secret_ids; + $project_get_by_ids_request->validate(); + $secrets_command = new SecretsCommand(); + $secrets_command->get_by_ids = $project_get_by_ids_request->jsonSerialize(); + return $this->run_secret_command($secrets_command); + } + + public function list(string $organization_id): \stdClass + { + $secrets_list_request = new SecretIdentifiersRequest(); + $secrets_list_request->organizationId = $organization_id; + $secrets_list_request->validate(); + $secrets_command = new SecretsCommand(); + $secrets_command->list = $secrets_list_request->jsonSerialize(); + return $this->run_secret_command($secrets_command); + } + + public function create(string $key, string $note, string $organization_id, array $project_ids, string $value): \stdClass + { + $secrets_create_request = new SecretCreateRequest(); + $secrets_create_request->organizationId = $organization_id; + $secrets_create_request->projectIds = $project_ids; + $secrets_create_request->key = $key; + $secrets_create_request->note = $note; + $secrets_create_request->value = $value; + $secrets_create_request->validate(); + $secrets_command = new SecretsCommand(); + $secrets_command->create = $secrets_create_request->jsonSerialize(); + return $this->run_secret_command($secrets_command); + } + + public function update(string $id, string $key, string $note, string $organization_id, array $project_ids, string $value): \stdClass + { + $secrets_put_request = new SecretPutRequest(); + $secrets_put_request->id = $id; + $secrets_put_request->organizationId = $organization_id; + $secrets_put_request->projectIds = $project_ids; + $secrets_put_request->key = $key; + $secrets_put_request->note = $note; + $secrets_put_request->value = $value; + $secrets_put_request->validate(); + $secrets_command = new SecretsCommand(); + $secrets_command->update = $secrets_put_request->jsonSerialize(); + return $this->run_secret_command($secrets_command); + } + + public function delete(array $secrets_ids): \stdClass + { + $secrets_delete_request = new SecretsDeleteRequest(); + $secrets_delete_request->ids = $secrets_ids; + $secrets_delete_request->validate(); + $secrets_command = new SecretsCommand(); + $secrets_command->delete = $secrets_delete_request->jsonSerialize(); + return $this->run_secret_command($secrets_command); + } + + public function run_secret_command($secretsCommand): \stdClass + { + $command = new Command(); + $command->secrets = $secretsCommand; + return $this->commandRunner->run($command); + } +} diff --git a/languages/php/src/schemas/AccessTokenLoginRequest.php b/languages/php/src/schemas/AccessTokenLoginRequest.php new file mode 100644 index 000000000..a08805f92 --- /dev/null +++ b/languages/php/src/schemas/AccessTokenLoginRequest.php @@ -0,0 +1,39 @@ +accessToken = Schema::string(); + $properties->accessToken->description = "Bitwarden service API access token"; + $ownerSchema->type = Schema::OBJECT; + $ownerSchema->additionalProperties = false; + $ownerSchema->description = "Login to Bitwarden with access token"; + $ownerSchema->required = array( + self::names()->accessToken, + ); + $ownerSchema->setFromRef('#/definitions/AccessTokenLoginRequest'); + } +} diff --git a/languages/php/src/schemas/BitwardenClassStructure.php b/languages/php/src/schemas/BitwardenClassStructure.php new file mode 100644 index 000000000..fd50354d4 --- /dev/null +++ b/languages/php/src/schemas/BitwardenClassStructure.php @@ -0,0 +1,11 @@ +properties = $properties; + $schema->objectItemClass = $className; + $schemaWrapper = new Wrapper($schema); + static::setUpProperties($properties, $schema); + if (null === $schema->getFromRefs()) { + $schema->setFromRef('#/definitions/' . $className); + } + if ($properties->isEmpty()) { + $schema->properties = null; + } + $properties->lock(); + } + + return $schemaWrapper; + } + + /** + * @return Properties|static|null + */ + public static function properties() + { + return static::schema()->getProperties(); + } + + /** + * @param mixed $data + * @param Context $options + * @return static|mixed + * @throws \Swaggest\JsonSchema\Exception + * @throws \Swaggest\JsonSchema\InvalidValue + */ + public static function import($data, Context $options = null) + { + return static::schema()->in($data, $options); + } + + /** + * @param mixed $data + * @param Context $options + * @return mixed + * @throws \Swaggest\JsonSchema\InvalidValue + * @throws \Exception + */ + public static function export($data, Context $options = null) + { + return static::schema()->out($data, $options); + } + + /** + * @param ObjectItemContract $objectItem + * @return static + */ + public static function pick(ObjectItemContract $objectItem) + { + $className = get_called_class(); + return $objectItem->getNestedObject($className); + } + + /** + * @return static + */ + public static function create() + { + return new static; + } + + protected $__validateOnSet = true; // todo skip validation during import + + /** + * @return \stdClass + */ + #[\ReturnTypeWillChange] + public function jsonSerialize() + { + $result = new \stdClass(); + $schema = static::schema(); + $properties = $schema->getProperties(); + $processed = array(); + if (null !== $properties) { + foreach ($properties->getDataKeyMap() as $propertyName => $dataName) { + $value = $this->$propertyName ?? null; + + // Value is exported if exists. + if (null !== $value || array_key_exists($propertyName, $this->__arrayOfData)) { + $result->$dataName = $value; + $processed[$propertyName] = true; + continue; + } + + // Non-existent value is only exported if belongs to nullable property (having 'null' in type array). + $property = $schema->getProperty($propertyName); + if ($property instanceof Schema) { + $types = $property->type; + if ($types === Schema::NULL || (is_array($types) && in_array(Schema::NULL, $types))) { + $result->$dataName = $value; + } + } + } + } + foreach ($schema->getNestedPropertyNames() as $name) { + /** @var ObjectItem $nested */ + $nested = $this->$name; + if (null !== $nested) { + foreach ((array)$nested->jsonSerialize() as $key => $value) { + $result->$key = $value; + } + } + } + + if (!empty($this->__arrayOfData)) { + foreach ($this->__arrayOfData as $name => $value) { + if (!isset($processed[$name])) { + $result->$name = $this->{$name}; + } + } + } + + return $result; + } + + /** + * @return static|NameMirror + */ + public static function names(Properties $properties = null, $mapping = Schema::DEFAULT_MAPPING) + { + if ($properties !== null) { + return new NameMirror($properties->getDataKeyMap($mapping)); + } + + static $nameflector = null; + if (null === $nameflector) { + $nameflector = new NameMirror(); + } + return $nameflector; + } + + public function __set($name, $column) // todo nested schemas + { + if ($this->__validateOnSet) { + if ($property = static::schema()->getProperty($name)) { + $property->out($column); + } + } + $this->__arrayOfData[$name] = $column; + return $this; + } + + public static function className() + { + return get_called_class(); + } + + /** + * @throws \Exception + * @throws \Swaggest\JsonSchema\InvalidValue + */ + public function validate() + { + static::schema()->out($this); + } +} + diff --git a/languages/php/src/schemas/ClientSettings.php b/languages/php/src/schemas/ClientSettings.php new file mode 100644 index 000000000..c27cc3322 --- /dev/null +++ b/languages/php/src/schemas/ClientSettings.php @@ -0,0 +1,133 @@ +identityUrl = Schema::string(); + $properties->identityUrl->description = "The identity url of the targeted Bitwarden instance. Defaults to `https://identity.bitwarden.com`"; + $properties->identityUrl->default = "https://identity.bitwarden.com"; + $properties->apiUrl = Schema::string(); + $properties->apiUrl->description = "The api url of the targeted Bitwarden instance. Defaults to `https://api.bitwarden.com`"; + $properties->apiUrl->default = "https://api.bitwarden.com"; + $properties->userAgent = Schema::string(); + $properties->userAgent->description = "The user_agent to sent to Bitwarden. Defaults to `Bitwarden Rust-SDK`"; + $properties->userAgent->default = "Bitwarden Rust-SDK"; + $properties->deviceType = new Schema(); + $propertiesDeviceTypeAllOf0 = Schema::string(); + $propertiesDeviceTypeAllOf0->enum = array( + self::ANDROID, + self::I_OS, + self::CHROME_EXTENSION, + self::FIREFOX_EXTENSION, + self::OPERA_EXTENSION, + self::EDGE_EXTENSION, + self::WINDOWS_DESKTOP, + self::MAC_OS_DESKTOP, + self::LINUX_DESKTOP, + self::CHROME_BROWSER, + self::FIREFOX_BROWSER, + self::OPERA_BROWSER, + self::EDGE_BROWSER, + self::IE_BROWSER, + self::UNKNOWN_BROWSER, + self::ANDROID_AMAZON, + self::UWP, + self::SAFARI_BROWSER, + self::VIVALDI_BROWSER, + self::VIVALDI_EXTENSION, + self::SAFARI_EXTENSION, + self::SDK, + ); + $propertiesDeviceTypeAllOf0->setFromRef('#/definitions/DeviceType'); + $properties->deviceType->allOf[0] = $propertiesDeviceTypeAllOf0; + $properties->deviceType->description = "Device type to send to Bitwarden. Defaults to SDK"; + $properties->deviceType->default = "SDK"; + $ownerSchema->type = Schema::OBJECT; + $ownerSchema->additionalProperties = false; + $ownerSchema->schema = "http://json-schema.org/draft-07/schema#"; + $ownerSchema->title = "ClientSettings"; + $ownerSchema->description = "Basic client behavior settings. These settings specify the various targets and behavior of the Bitwarden Client. They are optional and uneditable once the client is initialized.\n\nDefaults to\n\n``` # use bitwarden::client::client_settings::{ClientSettings, DeviceType}; # use assert_matches::assert_matches; let settings = ClientSettings { identity_url: \"https://identity.bitwarden.com\".to_string(), api_url: \"https://api.bitwarden.com\".to_string(), user_agent: \"Bitwarden Rust-SDK\".to_string(), device_type: DeviceType::SDK, }; let default = ClientSettings::default(); assert_matches!(settings, default); ```\n\nTargets `localhost:8080` for debug builds."; + } +} diff --git a/languages/php/src/schemas/Command.php b/languages/php/src/schemas/Command.php new file mode 100644 index 000000000..cbd649c2f --- /dev/null +++ b/languages/php/src/schemas/Command.php @@ -0,0 +1,44 @@ +projects = ProjectsCommand::schema(); + $properties->secrets = SecretsCommand::schema(); + $properties->accessTokenLogin = AccessTokenLoginRequest::schema(); + + $ownerSchema->type = Schema::OBJECT; + $ownerSchema->additionalProperties = false; + + $ownerSchema->oneOf = array( + self::names()->projects, + self::names()->secrets, + self::names()->accessTokenLogin, + ); + } +} diff --git a/languages/php/src/schemas/ProjectCreateRequest.php b/languages/php/src/schemas/ProjectCreateRequest.php new file mode 100644 index 000000000..6a4e0f082 --- /dev/null +++ b/languages/php/src/schemas/ProjectCreateRequest.php @@ -0,0 +1,43 @@ +organizationId = Schema::string(); + $properties->organizationId->description = "Organization where the project will be created"; + $properties->organizationId->format = "uuid"; + $properties->name = Schema::string(); + $ownerSchema->type = Schema::OBJECT; + $ownerSchema->additionalProperties = false; + $ownerSchema->required = array( + self::names()->name, + self::names()->organizationId, + ); + $ownerSchema->setFromRef('#/definitions/ProjectCreateRequest'); + } +} diff --git a/languages/php/src/schemas/ProjectGetRequest.php b/languages/php/src/schemas/ProjectGetRequest.php new file mode 100644 index 000000000..972bf18ec --- /dev/null +++ b/languages/php/src/schemas/ProjectGetRequest.php @@ -0,0 +1,37 @@ +id = Schema::string(); + $properties->id->description = "ID of the project to retrieve"; + $properties->id->format = "uuid"; + $ownerSchema->type = Schema::OBJECT; + $ownerSchema->additionalProperties = false; + $ownerSchema->required = array( + self::names()->id, + ); + $ownerSchema->setFromRef('#/definitions/ProjectGetRequest'); + } +} diff --git a/languages/php/src/schemas/ProjectPutRequest.php b/languages/php/src/schemas/ProjectPutRequest.php new file mode 100644 index 000000000..96b9705e7 --- /dev/null +++ b/languages/php/src/schemas/ProjectPutRequest.php @@ -0,0 +1,50 @@ +id = Schema::string(); + $properties->id->description = "ID of the project to modify"; + $properties->id->format = "uuid"; + $properties->organizationId = Schema::string(); + $properties->organizationId->description = "Organization ID of the project to modify"; + $properties->organizationId->format = "uuid"; + $properties->name = Schema::string(); + $ownerSchema->type = Schema::OBJECT; + $ownerSchema->additionalProperties = false; + $ownerSchema->required = array( + self::names()->id, + self::names()->name, + self::names()->organizationId, + ); + $ownerSchema->setFromRef('#/definitions/ProjectPutRequest'); + } +} diff --git a/languages/php/src/schemas/ProjectsCommand.php b/languages/php/src/schemas/ProjectsCommand.php new file mode 100644 index 000000000..22645db3c --- /dev/null +++ b/languages/php/src/schemas/ProjectsCommand.php @@ -0,0 +1,55 @@ + Requires Authentication > Requires using an Access Token for login or calling Sync at least once Deletes all the projects whose IDs match the provided ones + * + * Returns: [ProjectsDeleteResponse](bitwarden::secrets_manager::projects::ProjectsDeleteResponse) + */ +class ProjectsCommand extends BitwardenClassStructure +{ + public ?\stdClass $delete; + + public ?\stdClass $get; + + public ?\stdClass $list; + + public ?\stdClass $create; + + public ?\stdClass $update; + + + /** + * @param Properties|static $properties + * @param Schema $ownerSchema + */ + public static function setUpProperties($properties, Schema $ownerSchema) + { + $properties->delete = ProjectsDeleteRequest::schema() ? ProjectsDeleteRequest::schema() : null; + $properties->get = ProjectGetRequest::schema() ? ProjectGetRequest::schema() : null; + $properties->list = ProjectsListRequest::schema() ? ProjectsListRequest::schema() : null; + $properties->update = ProjectPutRequest::schema() ? ProjectPutRequest::schema() : null; + $properties->create = ProjectCreateRequest::schema() ? ProjectCreateRequest::schema() : null; + $ownerSchema->type = Schema::OBJECT; + $ownerSchema->additionalProperties = false; + $ownerSchema->description = "> Requires Authentication > Requires using an Access Token for login or calling Sync at least once Deletes all the projects whose IDs match the provided ones\n\nReturns: [ProjectsDeleteResponse](bitwarden::secrets_manager::projects::ProjectsDeleteResponse)"; + + $ownerSchema->oneOf = array( + self::names()->create, + self::names()->delete, + self::names()->get, + self::names()->list, + self::names()->update, + ); + } +} diff --git a/languages/php/src/schemas/ProjectsDeleteRequest.php b/languages/php/src/schemas/ProjectsDeleteRequest.php new file mode 100644 index 000000000..87a7cfad7 --- /dev/null +++ b/languages/php/src/schemas/ProjectsDeleteRequest.php @@ -0,0 +1,39 @@ +ids = Schema::arr(); + $properties->ids->items = Schema::string(); + $properties->ids->items->format = "uuid"; + $properties->ids->description = "IDs of the projects to delete"; + $ownerSchema->type = Schema::OBJECT; + $ownerSchema->additionalProperties = false; + $ownerSchema->required = array( + self::names()->ids, + ); + $ownerSchema->setFromRef('#/definitions/ProjectsDeleteRequest'); + } +} diff --git a/languages/php/src/schemas/ProjectsListRequest.php b/languages/php/src/schemas/ProjectsListRequest.php new file mode 100644 index 000000000..cc1a9474f --- /dev/null +++ b/languages/php/src/schemas/ProjectsListRequest.php @@ -0,0 +1,38 @@ +organizationId = Schema::string(); + $properties->organizationId->description = "Organization to retrieve all the projects from"; + $properties->organizationId->format = "uuid"; + $ownerSchema->type = Schema::OBJECT; + $ownerSchema->additionalProperties = false; + $ownerSchema->required = array( + self::names()->organizationId, + ); + $ownerSchema->setFromRef('#/definitions/ProjectsListRequest'); + } +} diff --git a/languages/php/src/schemas/SecretCreateRequest.php b/languages/php/src/schemas/SecretCreateRequest.php new file mode 100644 index 000000000..d34b36e98 --- /dev/null +++ b/languages/php/src/schemas/SecretCreateRequest.php @@ -0,0 +1,58 @@ +organizationId = Schema::string(); + $properties->organizationId->description = "Organization where the secret will be created"; + $properties->organizationId->format = "uuid"; + $properties->key = Schema::string(); + $properties->value = Schema::string(); + $properties->note = Schema::string(); + $properties->projectIds = (new Schema())->setType([Schema::_ARRAY, Schema::NULL]); + $properties->projectIds->items = Schema::string(); + $properties->projectIds->items->format = "uuid"; + $properties->projectIds->description = "IDs of the projects that this secret will belong to"; + $ownerSchema->type = Schema::OBJECT; + $ownerSchema->additionalProperties = false; + $ownerSchema->required = array( + self::names()->key, + self::names()->note, + self::names()->organizationId, + self::names()->value, + ); + $ownerSchema->setFromRef('#/definitions/SecretCreateRequest'); + } +} diff --git a/languages/php/src/schemas/SecretGetRequest.php b/languages/php/src/schemas/SecretGetRequest.php new file mode 100644 index 000000000..f31f7cad3 --- /dev/null +++ b/languages/php/src/schemas/SecretGetRequest.php @@ -0,0 +1,38 @@ +id = Schema::string(); + $properties->id->description = "ID of the secret to retrieve"; + $properties->id->format = "uuid"; + $ownerSchema->type = Schema::OBJECT; + $ownerSchema->additionalProperties = false; + $ownerSchema->required = array( + self::names()->id, + ); + $ownerSchema->setFromRef('#/definitions/SecretGetRequest'); + } +} diff --git a/languages/php/src/schemas/SecretIdentifiersRequest.php b/languages/php/src/schemas/SecretIdentifiersRequest.php new file mode 100644 index 000000000..b4e75b801 --- /dev/null +++ b/languages/php/src/schemas/SecretIdentifiersRequest.php @@ -0,0 +1,38 @@ +organizationId = Schema::string(); + $properties->organizationId->description = "Organization to retrieve all the secrets from"; + $properties->organizationId->format = "uuid"; + $ownerSchema->type = Schema::OBJECT; + $ownerSchema->additionalProperties = false; + $ownerSchema->required = array( + self::names()->organizationId, + ); + $ownerSchema->setFromRef('#/definitions/SecretIdentifiersRequest'); + } +} diff --git a/languages/php/src/schemas/SecretPutRequest.php b/languages/php/src/schemas/SecretPutRequest.php new file mode 100644 index 000000000..d890a909d --- /dev/null +++ b/languages/php/src/schemas/SecretPutRequest.php @@ -0,0 +1,64 @@ +id = Schema::string(); + $properties->id->description = "ID of the secret to modify"; + $properties->id->format = "uuid"; + $properties->organizationId = Schema::string(); + $properties->organizationId->description = "Organization ID of the secret to modify"; + $properties->organizationId->format = "uuid"; + $properties->key = Schema::string(); + $properties->value = Schema::string(); + $properties->note = Schema::string(); + $properties->projectIds = (new Schema())->setType([Schema::_ARRAY, Schema::NULL]); + $properties->projectIds->items = Schema::string(); + $properties->projectIds->items->format = "uuid"; + $ownerSchema->type = Schema::OBJECT; + $ownerSchema->additionalProperties = false; + $ownerSchema->required = array( + self::names()->id, + self::names()->key, + self::names()->note, + self::names()->organizationId, + self::names()->value, + ); + $ownerSchema->setFromRef('#/definitions/SecretPutRequest'); + } +} diff --git a/languages/php/src/schemas/SecretVerificationRequest.php b/languages/php/src/schemas/SecretVerificationRequest.php new file mode 100644 index 000000000..95cfd1e15 --- /dev/null +++ b/languages/php/src/schemas/SecretVerificationRequest.php @@ -0,0 +1,35 @@ +masterPassword = (new Schema())->setType([Schema::STRING, Schema::NULL]); + $properties->masterPassword->description = "The user's master password to use for user verification. If supplied, this will be used for verification purposes."; + $properties->otp = (new Schema())->setType([Schema::STRING, Schema::NULL]); + $properties->otp->description = "Alternate user verification method through OTP. This is provided for users who have no master password due to use of Customer Managed Encryption. Must be present and valid if master_password is absent."; + $ownerSchema->type = Schema::OBJECT; + $ownerSchema->additionalProperties = false; + $ownerSchema->setFromRef('#/definitions/SecretVerificationRequest'); + } +} diff --git a/languages/php/src/schemas/SecretsCommand.php b/languages/php/src/schemas/SecretsCommand.php new file mode 100644 index 000000000..1ed8c97c5 --- /dev/null +++ b/languages/php/src/schemas/SecretsCommand.php @@ -0,0 +1,56 @@ + Requires Authentication > Requires using an Access Token for login or calling Sync at least once Deletes all the secrets whose IDs match the provided ones + * + * Returns: [SecretsDeleteResponse](bitwarden::secrets_manager::secrets::SecretsDeleteResponse) + */ +class SecretsCommand extends BitwardenClassStructure +{ + public ?\stdClass $delete; + + public ?\stdClass $get; + + public ?\stdClass $getByIds; + + public ?\stdClass $list; + + public ?\stdClass $create; + + public ?\stdClass $put; + + /** + * @param Properties|static $properties + * @param Schema $ownerSchema + */ + public static function setUpProperties($properties, Schema $ownerSchema) + { + $properties->delete = SecretsDeleteRequest::schema() ? SecretsDeleteRequest::schema() : null; + $properties->getByIds = SecretsGetRequest::schema() ? SecretGetRequest::schema() : null; + $properties->create = SecretCreateRequest::schema() ? SecretCreateRequest::schema() : null; + $properties->put = SecretPutRequest::schema() ? SecretPutRequest::schema() : null; + $properties->list = SecretIdentifiersRequest::schema() ? SecretIdentifiersRequest::schema() : null; + $properties->get = SecretsGetRequest::schema() ? SecretGetRequest::schema() : null; + $ownerSchema->type = Schema::OBJECT; + $ownerSchema->additionalProperties = false; + $ownerSchema->description = "> Requires Authentication > Requires using an Access Token for login or calling Sync at least once Deletes all the secrets whose IDs match the provided ones\n\nReturns: [SecretsDeleteResponse](bitwarden::secrets_manager::secrets::SecretsDeleteResponse)"; + $ownerSchema->oneOf = array( + self::names()->create, + self::names()->put, + self::names()->list, + self::names()->getByIds, + self::names()->delete, + ); + } +} diff --git a/languages/php/src/schemas/SecretsDeleteRequest.php b/languages/php/src/schemas/SecretsDeleteRequest.php new file mode 100644 index 000000000..35138fcb1 --- /dev/null +++ b/languages/php/src/schemas/SecretsDeleteRequest.php @@ -0,0 +1,39 @@ +ids = Schema::arr(); + $properties->ids->items = Schema::string(); + $properties->ids->items->format = "uuid"; + $properties->ids->description = "IDs of the secrets to delete"; + $ownerSchema->type = Schema::OBJECT; + $ownerSchema->additionalProperties = false; + $ownerSchema->required = array( + self::names()->ids, + ); + $ownerSchema->setFromRef('#/definitions/SecretsDeleteRequest'); + } +} diff --git a/languages/php/src/schemas/SecretsGetRequest.php b/languages/php/src/schemas/SecretsGetRequest.php new file mode 100644 index 000000000..4758dabf4 --- /dev/null +++ b/languages/php/src/schemas/SecretsGetRequest.php @@ -0,0 +1,39 @@ +ids = Schema::arr(); + $properties->ids->items = Schema::string(); + $properties->ids->items->format = "uuid"; + $properties->ids->description = "IDs of the secrets to retrieve"; + $ownerSchema->type = Schema::OBJECT; + $ownerSchema->additionalProperties = false; + $ownerSchema->required = array( + self::names()->ids, + ); + $ownerSchema->setFromRef('#/definitions/SecretsGetRequest'); + } +} diff --git a/languages/python/BitwardenClient/bitwarden_client.py b/languages/python/BitwardenClient/bitwarden_client.py deleted file mode 100644 index 6a0a77ed8..000000000 --- a/languages/python/BitwardenClient/bitwarden_client.py +++ /dev/null @@ -1,84 +0,0 @@ -import json -from typing import Any, List -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 - - -class BitwardenClient: - def __init__(self, settings: ClientSettings = None): - if settings is None: - self.inner = bitwarden_py.BitwardenClient(None) - else: - 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)) - ) - 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 _run_command(self, command: Command) -> Any: - response_json = self.inner.run_command(json.dumps(command.to_dict())) - return json.loads(response_json) - - -class SecretsClient: - def __init__(self, client: BitwardenClient): - self.client = client - - def get(self, id: str) -> ResponseForSecretResponse: - result = self.client._run_command( - Command(secrets=SecretsCommand(get=SecretGetRequest(id))) - ) - return ResponseForSecretResponse.from_dict(result) - - def create(self, key: str, - note: str, - organization_id: str, - value: str) -> ResponseForSecretResponse: - result = self.client._run_command( - Command(secrets=SecretsCommand( - create=SecretCreateRequest(key, note, organization_id, value))) - ) - return ResponseForSecretResponse.from_dict(result) - - def list(self, organization_id: str) -> ResponseForSecretIdentifiersResponse: - result = self.client._run_command( - Command(secrets=SecretsCommand( - list=SecretIdentifiersRequest(organization_id))) - ) - return ResponseForSecretIdentifiersResponse.from_dict(result) - - def update(self, id: str, - key: str, - note: str, - organization_id: str, - value: str) -> ResponseForSecretResponse: - result = self.client._run_command( - Command(secrets=SecretsCommand(update=SecretPutRequest( - id, key, note, organization_id, value))) - ) - return ResponseForSecretResponse.from_dict(result) - - def delete(self, ids: List[str]) -> ResponseForSecretsDeleteResponse: - result = self.client._run_command( - Command(secrets=SecretsCommand(delete=SecretsDeleteRequest(ids))) - ) - return ResponseForSecretsDeleteResponse.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/BitwardenClient/__init__.py b/languages/python/bitwarden_sdk/__init__.py similarity index 100% rename from languages/python/BitwardenClient/__init__.py rename to languages/python/bitwarden_sdk/__init__.py diff --git a/languages/python/bitwarden_sdk/bitwarden_client.py b/languages/python/bitwarden_sdk/bitwarden_client.py new file mode 100644 index 000000000..cb669a40e --- /dev/null +++ b/languages/python/bitwarden_sdk/bitwarden_client.py @@ -0,0 +1,124 @@ +import json +from typing import Any, List, Optional +from uuid import UUID +import bitwarden_py +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): + if settings is None: + self.inner = bitwarden_py.BitwardenClient(None) + else: + settings_json = json.dumps(settings.to_dict()) + self.inner = bitwarden_py.BitwardenClient(settings_json) + + def access_token_login(self, access_token: str): + self._run_command( + Command(access_token_login=AccessTokenLoginRequest(access_token)) + ) + + 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())) + response = json.loads(response_json) + + if response["success"] == False: + raise Exception(response["errorMessage"]) + + return response + +class SecretsClient: + def __init__(self, client: BitwardenClient): + self.client = client + + def get(self, id: str) -> ResponseForSecretResponse: + result = self.client._run_command( + Command(secrets=SecretsCommand(get=SecretGetRequest(id))) + ) + return ResponseForSecretResponse.from_dict(result) + + def create(self, key: str, + note: str, + organization_id: str, + value: str, + project_ids: Optional[List[UUID]] = None + ) -> ResponseForSecretResponse: + result = self.client._run_command( + Command(secrets=SecretsCommand( + create=SecretCreateRequest(key, note, organization_id, value, project_ids))) + ) + return ResponseForSecretResponse.from_dict(result) + + def list(self, organization_id: str) -> ResponseForSecretIdentifiersResponse: + result = self.client._run_command( + Command(secrets=SecretsCommand( + list=SecretIdentifiersRequest(organization_id))) + ) + return ResponseForSecretIdentifiersResponse.from_dict(result) + + def update(self, id: str, + key: str, + note: str, + organization_id: str, + 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, project_ids))) + ) + return ResponseForSecretResponse.from_dict(result) + + def delete(self, ids: List[str]) -> ResponseForSecretsDeleteResponse: + result = self.client._run_command( + 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/example.py b/languages/python/example.py new file mode 100644 index 000000000..b3f2ab006 --- /dev/null +++ b/languages/python/example.py @@ -0,0 +1,44 @@ +import json +import logging +import sys +from bitwarden_sdk.bitwarden_client import BitwardenClient +from bitwarden_sdk.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/python/setup.py b/languages/python/setup.py index 7976ebcba..b243a4fe8 100644 --- a/languages/python/setup.py +++ b/languages/python/setup.py @@ -2,11 +2,11 @@ from setuptools_rust import Binding, RustExtension setup( - name="BitwardenClient", + name="bitwarden_sdk", description="A Bitwarden Client for python", version="0.1", rust_extensions=[RustExtension( "bitwarden_py", path="../../crates/bitwarden-py/Cargo.toml", binding=Binding.PyO3)], - packages=['bitwardenclient'], + packages=['bitwarden_sdk'], zip_safe=False, ) 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/package-lock.json b/package-lock.json index 6ee8982ec..bb67d4e04 100644 --- a/package-lock.json +++ b/package-lock.json @@ -12,16 +12,16 @@ "@openapitools/openapi-generator-cli": "2.7.0", "handlebars": "^4.7.8", "prettier": "3.1.0", - "quicktype-core": "23.0.79", + "quicktype-core": "23.0.80", "rimraf": "5.0.5", "ts-node": "10.9.1", - "typescript": "5.2.2" + "typescript": "5.3.2" } }, "node_modules/@babel/runtime": { - "version": "7.23.2", - "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.23.2.tgz", - "integrity": "sha512-mM8eg4yl5D6i3lu2QKPuPH4FArvJ8KhTofbE7jwMUv9KX5mBvwPAqnV3MlyBNqdp9RyRKP6Yck8TrfYrPvX3bg==", + "version": "7.23.5", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.23.5.tgz", + "integrity": "sha512-NdUTHcPe4C99WxPub+K9l9tK5/lV4UXIoaHSYgzco9BCyjKAAwzdBI+wWtYqHt7LJdbo74ZjRPJgzVweq1sz0w==", "dev": true, "dependencies": { "regenerator-runtime": "^0.14.0" @@ -363,9 +363,9 @@ "dev": true }, "node_modules/@types/node": { - "version": "20.9.2", - "resolved": "https://registry.npmjs.org/@types/node/-/node-20.9.2.tgz", - "integrity": "sha512-WHZXKFCEyIUJzAwh3NyyTHYSR35SevJ6mZ1nWwJafKtiQbqRTIKSRcw3Ma3acqgsent3RRDqeVwpHntMk+9irg==", + "version": "20.10.3", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.10.3.tgz", + "integrity": "sha512-XJavIpZqiXID5Yxnxv3RUDKTN5b81ddNC3ecsA0SoFXz/QU8OGBwZGMomiq0zw+uuqbL/krztv/DINAQ/EV4gg==", "dev": true, "peer": true, "dependencies": { @@ -373,9 +373,9 @@ } }, "node_modules/@types/urijs": { - "version": "1.19.23", - "resolved": "https://registry.npmjs.org/@types/urijs/-/urijs-1.19.23.tgz", - "integrity": "sha512-3Zbk6RzmIpvKTNEHO2RcPOGHM++BQEITMqBRR1Ju32WbruhV/pygYgxiP3xA0b1B88zjzs0Izzjxsbj768+IjA==", + "version": "1.19.25", + "resolved": "https://registry.npmjs.org/@types/urijs/-/urijs-1.19.25.tgz", + "integrity": "sha512-XOfUup9r3Y06nFAZh3WvO0rBU4OtlfPB/vgxpjg+NRdGU6CN6djdc6OEiH+PcqHCY6eFLo9Ista73uarf4gnBg==", "dev": true }, "node_modules/abort-controller": { @@ -584,9 +584,9 @@ } }, "node_modules/cli-spinners": { - "version": "2.9.1", - "resolved": "https://registry.npmjs.org/cli-spinners/-/cli-spinners-2.9.1.tgz", - "integrity": "sha512-jHgecW0pxkonBJdrKsqxgRX9AcG+u/5k0Q7WPDfi8AogLAdwxEkyYYNWwZ5GvVFoFx2uiY1eNcSK00fh+1+FyQ==", + "version": "2.9.2", + "resolved": "https://registry.npmjs.org/cli-spinners/-/cli-spinners-2.9.2.tgz", + "integrity": "sha512-ywqV+5MmyL4E7ybXgKys4DugZbX0FC6LnwrhjuykIjnK9k8OQacQ7axGKnjDXWNhns0xot3bZI5h55H8yo9cJg==", "dev": true, "engines": { "node": ">=6" @@ -1252,9 +1252,9 @@ } }, "node_modules/lru-cache": { - "version": "10.0.3", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.0.3.tgz", - "integrity": "sha512-B7gr+F6MkqB3uzINHXNctGieGsRTMwIBgxkp0yq/5BwcuDzD4A8wQpHQW6vDAm1uKSLQghmRdD9sKqf2vJ1cEg==", + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.1.0.tgz", + "integrity": "sha512-/1clY/ui8CzjKFyjdvwPWJUYKiFVXG2I2cY0ssG7h4+hwk+XOIX7ZSG9Q7TW8TW3Kp3BUSqgFWBLgL4PJ+Blag==", "dev": true, "engines": { "node": "14 || >=16.14" @@ -1494,9 +1494,9 @@ } }, "node_modules/quicktype-core": { - "version": "23.0.79", - "resolved": "https://registry.npmjs.org/quicktype-core/-/quicktype-core-23.0.79.tgz", - "integrity": "sha512-Auzy8AhorFt6YGeB53/dzUSINmKKassAyCsr2wpNgG9XoC3i6oUoLuySNUzYIkyCFCGmKdBRBQeyAqPOVteoYw==", + "version": "23.0.80", + "resolved": "https://registry.npmjs.org/quicktype-core/-/quicktype-core-23.0.80.tgz", + "integrity": "sha512-dd+aJRzAl3MzkaXJMjUu0j60y82gwX/RRr3EvW/aScQKycvkgwliNDN2tIiLB06EKBzjgC9mtlMqKyRg2rYKhQ==", "dev": true, "dependencies": { "@glideapps/ts-necessities": "2.1.3", @@ -1936,9 +1936,9 @@ } }, "node_modules/typescript": { - "version": "5.2.2", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.2.2.tgz", - "integrity": "sha512-mI4WrpHsbCIcwT9cF4FZvr80QUeKvsUsUvKDoR+X/7XHQH98xYD8YHZg7ANtz2GtZt/CBq2QJ0thkGJMHfqc1w==", + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.3.2.tgz", + "integrity": "sha512-6l+RyNy7oAHDfxC4FzSJcz9vnjTKxrLpDG5M2Vu4SHRVNg6xzqZp6LYSR9zjqQTu8DU/f5xwxUdADOkbrIX2gQ==", "dev": true, "bin": { "tsc": "bin/tsc", diff --git a/package.json b/package.json index 97f08c571..363d8b34c 100644 --- a/package.json +++ b/package.json @@ -23,9 +23,9 @@ "@openapitools/openapi-generator-cli": "2.7.0", "handlebars": "^4.7.8", "prettier": "3.1.0", - "quicktype-core": "23.0.79", + "quicktype-core": "23.0.80", "rimraf": "5.0.5", "ts-node": "10.9.1", - "typescript": "5.2.2" + "typescript": "5.3.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 4c96b21f7..f045b0f5a 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"; @@ -16,22 +22,16 @@ async function* walk(dir: string): AsyncIterable { async function main() { const schemaInput = new JSONSchemaInput(new FetchingJSONSchemaStore()); - - const filenames: string[] = []; - for await (const p of walk("./support/schemas")) { - filenames.push(p); - } - - filenames.sort(); - - for (const f of filenames) { - const buffer = fs.readFileSync(f); - const relative = path.relative(path.join(process.cwd(), "support/schemas"), f); - await schemaInput.addSource({ name: relative, schema: buffer.toString() }); - } - const inputData = new InputData(); inputData.addInput(schemaInput); + inputData.addSource( + "schema", + { + name: "SchemaTypes", + uris: ["support/schemas/schema_types/SchemaTypes.json#/definitions/"], + }, + () => new JSONSchemaInput(new FetchingJSONSchemaStore()), + ); const ts = await quicktype({ inputData, @@ -50,7 +50,17 @@ async function main() { }, }); - writeToFile("./languages/python/BitwardenClient/schemas.py", python.lines); + writeToFile("./languages/python/bitwarden_sdk/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, @@ -63,6 +73,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();