diff --git a/.github/renovate.json b/.github/renovate.json index 42f5f93f2..ae4ad0e9d 100644 --- a/.github/renovate.json +++ b/.github/renovate.json @@ -2,6 +2,7 @@ "$schema": "https://docs.renovatebot.com/renovate-schema.json", "extends": [ "config:base", + "github>bitwarden/renovate-config:pin-actions", ":combinePatchMinorReleases", ":dependencyDashboard", ":maintainLockFilesWeekly", diff --git a/.github/workflows/build-android.yml b/.github/workflows/build-android.yml index f8bee2f56..ce08322be 100644 --- a/.github/workflows/build-android.yml +++ b/.github/workflows/build-android.yml @@ -30,15 +30,15 @@ jobs: - name: Install rust uses: dtolnay/rust-toolchain@be73d7920c329f220ce78e0234b8f96b7ae60248 # stable with: - toolchain: 1.67.0 # https://github.com/cross-rs/cross/issues/1222 + toolchain: stable - name: Cache cargo registry - uses: Swatinem/rust-cache@3cf7f8cc28d1b4e7d01e3783be10a97d55d483c8 # v2.7.1 + uses: Swatinem/rust-cache@23bce251a8cd2ffc3c1075eaa2367cf899916d84 # v2.7.3 with: key: ${{ matrix.settings.target }}-cargo - name: Install Cross - run: cargo install cross --locked + run: cargo install cross --locked --git https://github.com/cross-rs/cross.git --rev 185398b1b885820515a212de720a306b08e2c8c9 - name: Build env: @@ -46,7 +46,7 @@ jobs: run: cross build -p bitwarden-uniffi --release --target=${{ matrix.settings.target }} - name: Upload artifact - uses: actions/upload-artifact@c7d193f32edcb7bfad88892161225aeda64e9392 # v4.0.0 + uses: actions/upload-artifact@694cdabd8bdb0f10b2cea11669e1bf5453eed0a6 # v4.2.0 with: name: android-${{ matrix.settings.target }} path: ./target/${{ matrix.settings.target }}/release/libbitwarden_uniffi.so @@ -75,7 +75,7 @@ jobs: toolchain: stable - name: Cache cargo registry - uses: Swatinem/rust-cache@3cf7f8cc28d1b4e7d01e3783be10a97d55d483c8 # v2.7.1 + uses: Swatinem/rust-cache@23bce251a8cd2ffc3c1075eaa2367cf899916d84 # v2.7.3 with: key: cargo-combine-cache @@ -86,7 +86,7 @@ jobs: java-version: 17 - name: Download Artifacts - uses: actions/download-artifact@f44cd7b40bfd40b6aa1cc1b9b5b7bf03d3c67110 # v4.1.0 + uses: actions/download-artifact@6b208ae046db98c579e8a3aa621ab581ff575935 # v4.1.1 - name: Move artifacts working-directory: languages/kotlin/sdk/src/main/jniLibs diff --git a/.github/workflows/build-cli-docker.yml b/.github/workflows/build-cli-docker.yml new file mode 100644 index 000000000..4605da5e5 --- /dev/null +++ b/.github/workflows/build-cli-docker.yml @@ -0,0 +1,163 @@ +--- +name: Build bws Docker image + +on: + push: + paths: + - "crates/bws/**" + workflow_dispatch: + inputs: + sdk_branch: + description: "Server branch name to deploy (examples: 'master', 'rc', 'feature/sm')" + type: string + default: master + pull_request: + paths: + - ".github/workflows/build-cli-docker.yml" + - "crates/bws/**" + +env: + _AZ_REGISTRY: bitwardenprod.azurecr.io + +jobs: + build-docker: + name: Build Docker image + runs-on: ubuntu-22.04 + steps: + - name: Checkout Repository + uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 + + - name: Check Branch to Publish + env: + PUBLISH_BRANCHES: "master,rc,hotfix-rc" + id: publish-branch-check + run: | + REF=${GITHUB_HEAD_REF:-${GITHUB_REF#refs/heads/}} + + IFS="," read -a publish_branches <<< $PUBLISH_BRANCHES + + if [[ "${publish_branches[*]}" =~ "${REF}" ]]; then + echo "is_publish_branch=true" >> $GITHUB_ENV + else + echo "is_publish_branch=false" >> $GITHUB_ENV + fi + + ########## Set up Docker ########## + - name: Set up QEMU emulators + uses: docker/setup-qemu-action@2b82ce82d56a2a04d2637cd93a637ae1b359c0a7 # v2.2.0 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@885d1462b80bc1c1c7f0b00334ad271f09369c55 # v2.10.0 + + ########## Login to Docker registries ########## + - name: Login to Azure - Prod Subscription + uses: Azure/login@e15b166166a8746d1a47596803bd8c1b595455cf # v1.6.0 + with: + creds: ${{ secrets.AZURE_PROD_KV_CREDENTIALS }} + + - name: Login to Azure ACR + run: az acr login -n ${_AZ_REGISTRY%.azurecr.io} + + - name: Login to Azure - CI Subscription + uses: Azure/login@e15b166166a8746d1a47596803bd8c1b595455cf # v1.6.0 + with: + creds: ${{ secrets.AZURE_KV_CI_SERVICE_PRINCIPAL }} + + - name: Retrieve github PAT secrets + id: retrieve-secret-pat + uses: bitwarden/gh-actions/get-keyvault-secrets@main + with: + keyvault: "bitwarden-ci" + secrets: "github-pat-bitwarden-devops-bot-repo-scope" + + - name: Setup Docker Trust + if: ${{ env.is_publish_branch == 'true' }} + uses: bitwarden/gh-actions/setup-docker-trust@main + with: + azure-creds: ${{ secrets.AZURE_KV_CI_SERVICE_PRINCIPAL }} + azure-keyvault-name: "bitwarden-ci" + + ########## Generate image tag and build Docker image ########## + - name: Generate Docker image tag + id: tag + run: | + REF=${GITHUB_HEAD_REF:-${GITHUB_REF#refs/heads/}} + IMAGE_TAG=$(echo "${REF}" | sed "s#/#-#g") # slash safe branch name + if [[ "${IMAGE_TAG}" == "master" ]]; then + IMAGE_TAG=dev + elif [[ ("${IMAGE_TAG}" == "rc") || ("${IMAGE_TAG}" == "hotfix-rc") ]]; then + IMAGE_TAG=rc + fi + + echo "image_tag=${IMAGE_TAG}" >> $GITHUB_OUTPUT + + - name: Generate tag list + id: tag-list + env: + IMAGE_TAG: ${{ steps.tag.outputs.image_tag }} + IS_PUBLISH_BRANCH: ${{ env.is_publish_branch }} + run: | + if [[ ("${IMAGE_TAG}" == "dev" || "${IMAGE_TAG}" == "rc") && "${IS_PUBLISH_BRANCH}" == "true" ]]; then + echo "tags=$_AZ_REGISTRY/bws:${IMAGE_TAG},bitwarden/bws:${IMAGE_TAG}" >> $GITHUB_OUTPUT + else + echo "tags=$_AZ_REGISTRY/bws:${IMAGE_TAG}" >> $GITHUB_OUTPUT + fi + + - name: Build and push Docker image + uses: docker/build-push-action@1104d471370f9806843c095c1db02b5a90c5f8b6 # v3.3.1 + with: + context: . + file: crates/bws/Dockerfile + platforms: | + linux/amd64, + linux/arm64/v8 + push: true + tags: ${{ steps.tag-list.outputs.tags }} + secrets: | + "GH_PAT=${{ steps.retrieve-secret-pat.outputs.github-pat-bitwarden-devops-bot-repo-scope }}" + + - name: Log out of Docker and disable Docker Notary + if: ${{ env.is_publish_branch == 'true' }} + run: | + docker logout + echo "DOCKER_CONTENT_TRUST=0" >> $GITHUB_ENV + + check-failures: + name: Check for failures + if: always() + runs-on: ubuntu-22.04 + needs: build-docker + steps: + - name: Check if any job failed + if: | + github.ref == 'refs/heads/master' + || github.ref == 'refs/heads/rc' + || github.ref == 'refs/heads/hotfix-rc' + env: + BUILD_DOCKER_STATUS: ${{ needs.build-docker.result }} + run: | + if [ "$BUILD_DOCKER_STATUS" = "failure" ]; then + exit 1 + fi + + - name: Login to Azure - CI subscription + uses: Azure/login@e15b166166a8746d1a47596803bd8c1b595455cf # v1.6.0 + if: failure() + with: + creds: ${{ secrets.AZURE_KV_CI_SERVICE_PRINCIPAL }} + + - name: Retrieve secrets + id: retrieve-secrets + uses: bitwarden/gh-actions/get-keyvault-secrets@main + if: failure() + with: + keyvault: "bitwarden-ci" + secrets: "devops-alerts-slack-webhook-url" + + - name: Notify Slack on failure + uses: act10ns/slack@ed1309ab9862e57e9e583e51c7889486b9a00b0f # v2.0.0 + if: failure() + env: + SLACK_WEBHOOK_URL: ${{ steps.retrieve-secrets.outputs.devops-alerts-slack-webhook-url }} + with: + status: ${{ job.status }} diff --git a/.github/workflows/build-cli.yml b/.github/workflows/build-cli.yml index b0f3e7b63..1e8ad2896 100644 --- a/.github/workflows/build-cli.yml +++ b/.github/workflows/build-cli.yml @@ -69,13 +69,13 @@ jobs: targets: ${{ matrix.settings.target }} - name: Cache cargo registry - uses: Swatinem/rust-cache@3cf7f8cc28d1b4e7d01e3783be10a97d55d483c8 # v2.7.1 + uses: Swatinem/rust-cache@23bce251a8cd2ffc3c1075eaa2367cf899916d84 # v2.7.3 with: key: ${{ matrix.settings.target }}-cargo-${{ matrix.settings.os }} - name: Install Cross (aarch64-unknown-linux-gnu) if: ${{ matrix.settings.target == 'aarch64-unknown-linux-gnu' }} - run: cargo install cross --locked + run: cargo install cross --locked --git https://github.com/cross-rs/cross.git --rev 185398b1b885820515a212de720a306b08e2c8c9 - name: Build if: ${{ matrix.settings.target != 'aarch64-unknown-linux-gnu' }} @@ -99,7 +99,7 @@ jobs: run: zip -j ./bws-${{ matrix.settings.target }}-${{ env._PACKAGE_VERSION }}.zip ./target/${{ matrix.settings.target }}/release/bws - name: Upload artifact - uses: actions/upload-artifact@c7d193f32edcb7bfad88892161225aeda64e9392 # v4.0.0 + uses: actions/upload-artifact@694cdabd8bdb0f10b2cea11669e1bf5453eed0a6 # v4.2.0 with: name: bws-${{ matrix.settings.target }}-${{ env._PACKAGE_VERSION }}.zip path: ./bws-${{ matrix.settings.target }}-${{ env._PACKAGE_VERSION }}.zip @@ -118,12 +118,12 @@ jobs: uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 - name: Download x86_64-apple-darwin artifact - uses: actions/download-artifact@f44cd7b40bfd40b6aa1cc1b9b5b7bf03d3c67110 # v4.1.0 + uses: actions/download-artifact@6b208ae046db98c579e8a3aa621ab581ff575935 # v4.1.1 with: name: bws-x86_64-apple-darwin-${{ env._PACKAGE_VERSION }}.zip - name: Download aarch64-apple-darwin artifact - uses: actions/download-artifact@f44cd7b40bfd40b6aa1cc1b9b5b7bf03d3c67110 # v4.1.0 + uses: actions/download-artifact@6b208ae046db98c579e8a3aa621ab581ff575935 # v4.1.1 with: name: bws-aarch64-apple-darwin-${{ env._PACKAGE_VERSION }}.zip @@ -142,7 +142,7 @@ jobs: run: zip ./bws-macos-universal-${{ env._PACKAGE_VERSION }}.zip ./bws-macos-universal/bws - name: Upload artifact - uses: actions/upload-artifact@c7d193f32edcb7bfad88892161225aeda64e9392 # v4.0.0 + uses: actions/upload-artifact@694cdabd8bdb0f10b2cea11669e1bf5453eed0a6 # v4.2.0 with: name: bws-macos-universal-${{ env._PACKAGE_VERSION }}.zip path: ./bws-macos-universal-${{ env._PACKAGE_VERSION }}.zip @@ -163,7 +163,7 @@ jobs: toolchain: stable - name: Cache cargo registry - uses: Swatinem/rust-cache@3cf7f8cc28d1b4e7d01e3783be10a97d55d483c8 # v2.7.1 + uses: Swatinem/rust-cache@23bce251a8cd2ffc3c1075eaa2367cf899916d84 # v2.7.3 with: key: cargo-cli-about @@ -177,7 +177,7 @@ jobs: sed -i.bak 's/\$NAME\$/Bitwarden Secrets Manager CLI/g' THIRDPARTY.html - name: Upload artifact - uses: actions/upload-artifact@c7d193f32edcb7bfad88892161225aeda64e9392 # v4.0.0 + uses: actions/upload-artifact@694cdabd8bdb0f10b2cea11669e1bf5453eed0a6 # v4.2.0 with: name: THIRDPARTY.html path: ./crates/bws/THIRDPARTY.html diff --git a/.github/workflows/build-dotnet.yml b/.github/workflows/build-dotnet.yml index 904cea204..232efee9d 100644 --- a/.github/workflows/build-dotnet.yml +++ b/.github/workflows/build-dotnet.yml @@ -24,7 +24,7 @@ jobs: uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 - name: Download C# schemas artifact - uses: actions/download-artifact@f44cd7b40bfd40b6aa1cc1b9b5b7bf03d3c67110 # v4.1.0 + uses: actions/download-artifact@6b208ae046db98c579e8a3aa621ab581ff575935 # v4.1.1 with: name: schemas.cs path: languages/csharp/Bitwarden.Sdk @@ -35,25 +35,25 @@ jobs: global-json-file: languages/csharp/global.json - name: Download x86_64-apple-darwin files - uses: actions/download-artifact@f44cd7b40bfd40b6aa1cc1b9b5b7bf03d3c67110 # v4.1.0 + uses: actions/download-artifact@6b208ae046db98c579e8a3aa621ab581ff575935 # v4.1.1 with: name: libbitwarden_c_files-x86_64-apple-darwin path: languages/csharp/Bitwarden.Sdk/macos-x64 - name: Download aarch64-apple-darwin files - uses: actions/download-artifact@f44cd7b40bfd40b6aa1cc1b9b5b7bf03d3c67110 # v4.1.0 + uses: actions/download-artifact@6b208ae046db98c579e8a3aa621ab581ff575935 # v4.1.1 with: name: libbitwarden_c_files-aarch64-apple-darwin path: languages/csharp/Bitwarden.Sdk/macos-arm64 - name: Download x86_64-unknown-linux-gnu files - uses: actions/download-artifact@f44cd7b40bfd40b6aa1cc1b9b5b7bf03d3c67110 # v4.1.0 + uses: actions/download-artifact@6b208ae046db98c579e8a3aa621ab581ff575935 # v4.1.1 with: name: libbitwarden_c_files-x86_64-unknown-linux-gnu - path: languages/csharp/Bitwarden.Sdk/ubuntu-x64 + path: languages/csharp/Bitwarden.Sdk/linux-x64 - name: Download x86_64-pc-windows-msvc files - uses: actions/download-artifact@f44cd7b40bfd40b6aa1cc1b9b5b7bf03d3c67110 # v4.1.0 + uses: actions/download-artifact@6b208ae046db98c579e8a3aa621ab581ff575935 # v4.1.1 with: name: libbitwarden_c_files-x86_64-pc-windows-msvc path: languages/csharp/Bitwarden.Sdk/windows-x64 @@ -71,7 +71,7 @@ jobs: working-directory: languages/csharp/Bitwarden.Sdk - name: Upload NuGet package - uses: actions/upload-artifact@c7d193f32edcb7bfad88892161225aeda64e9392 # v4.0.0 + uses: actions/upload-artifact@694cdabd8bdb0f10b2cea11669e1bf5453eed0a6 # v4.2.0 with: name: Bitwarden.Sdk.0.0.1.nupkg path: | diff --git a/.github/workflows/build-java.yml b/.github/workflows/build-java.yml index d2bc049d1..3c574d823 100644 --- a/.github/workflows/build-java.yml +++ b/.github/workflows/build-java.yml @@ -4,6 +4,7 @@ on: push: branches: - main + workflow_dispatch: jobs: generate_schemas: @@ -24,7 +25,7 @@ jobs: uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 - name: Download Java schemas artifact - uses: actions/download-artifact@f44cd7b40bfd40b6aa1cc1b9b5b7bf03d3c67110 # v4.1.0 + uses: actions/download-artifact@6b208ae046db98c579e8a3aa621ab581ff575935 # v4.1.1 with: name: sdk-schemas-java path: languages/java/src/main/java/bit/sdk/schema/ @@ -36,28 +37,28 @@ jobs: java-version: 17 - name: Download x86_64-apple-darwin files - uses: actions/download-artifact@f44cd7b40bfd40b6aa1cc1b9b5b7bf03d3c67110 # v4.1.0 + uses: actions/download-artifact@6b208ae046db98c579e8a3aa621ab581ff575935 # v4.1.1 with: name: libbitwarden_c_files-x86_64-apple-darwin - path: languages/java/src/main/resources/darwin-x64 + path: languages/java/src/main/resources/darwin-x86-64 - name: Download aarch64-apple-darwin files - uses: actions/download-artifact@f44cd7b40bfd40b6aa1cc1b9b5b7bf03d3c67110 # v4.1.0 + uses: actions/download-artifact@6b208ae046db98c579e8a3aa621ab581ff575935 # v4.1.1 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@f44cd7b40bfd40b6aa1cc1b9b5b7bf03d3c67110 # v4.1.0 + uses: actions/download-artifact@6b208ae046db98c579e8a3aa621ab581ff575935 # v4.1.1 with: name: libbitwarden_c_files-x86_64-unknown-linux-gnu - path: languages/java/src/main/resources/ubuntu-x64 + path: languages/java/src/main/resources/linux-x86-64 - name: Download x86_64-pc-windows-msvc files - uses: actions/download-artifact@f44cd7b40bfd40b6aa1cc1b9b5b7bf03d3c67110 # v4.1.0 + uses: actions/download-artifact@6b208ae046db98c579e8a3aa621ab581ff575935 # v4.1.1 with: name: libbitwarden_c_files-x86_64-pc-windows-msvc - path: languages/java/src/main/resources/windows-x64 + path: languages/java/src/main/resources/win32-x86-64 - name: Publish Maven uses: gradle/gradle-build-action@982da8e78c05368c70dac0351bb82647a9e9a5d2 # v2.11.1 diff --git a/.github/workflows/build-napi.yml b/.github/workflows/build-napi.yml index ef72e7f1e..40e6ffaef 100644 --- a/.github/workflows/build-napi.yml +++ b/.github/workflows/build-napi.yml @@ -67,12 +67,12 @@ jobs: targets: ${{ matrix.settings.target }} - name: Cache cargo registry - uses: Swatinem/rust-cache@3cf7f8cc28d1b4e7d01e3783be10a97d55d483c8 # v2.7.1 + uses: Swatinem/rust-cache@23bce251a8cd2ffc3c1075eaa2367cf899916d84 # v2.7.3 with: key: ${{ matrix.settings.target }}-cargo-${{ matrix.settings.os }} - name: Retrieve schemas - uses: actions/download-artifact@f44cd7b40bfd40b6aa1cc1b9b5b7bf03d3c67110 # v4.1.0 + uses: actions/download-artifact@6b208ae046db98c579e8a3aa621ab581ff575935 # v4.1.1 with: name: schemas.ts path: ${{ github.workspace }}/crates/bitwarden-napi/src-ts/bitwarden_client/ @@ -84,7 +84,7 @@ jobs: run: ${{ matrix.settings.build }} - name: Upload artifact - uses: actions/upload-artifact@c7d193f32edcb7bfad88892161225aeda64e9392 # v4.0.0 + uses: actions/upload-artifact@694cdabd8bdb0f10b2cea11669e1bf5453eed0a6 # v4.2.0 with: name: sdk-bitwarden-napi-${{ matrix.settings.target }} path: ${{ github.workspace }}/crates/bitwarden-napi/sdk-napi.*.node diff --git a/.github/workflows/build-python-wheels.yml b/.github/workflows/build-python-wheels.yml new file mode 100644 index 000000000..dace2d047 --- /dev/null +++ b/.github/workflows/build-python-wheels.yml @@ -0,0 +1,122 @@ +--- +name: Build Python Wheels + +on: + pull_request: + push: + branches: + - "main" + - "rc" + - "hotfix-rc" + workflow_dispatch: + +defaults: + run: + shell: bash + working-directory: languages/python + +jobs: + generate_schemas: + uses: ./.github/workflows/generate_schemas.yml + + setup: + name: Setup + runs-on: ubuntu-22.04 + outputs: + package_version: ${{ steps.retrieve-version.outputs.package_version }} + steps: + - name: Checkout repo + uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 + + - name: Get Package Version + id: retrieve-version + run: | + VERSION="$(grep -o '^version = ".*"' ../../crates/bitwarden-py/Cargo.toml | grep -Eo "[0-9]+\.[0-9]+\.[0-9]+")" + echo "package_version=$VERSION" >> $GITHUB_OUTPUT + + build: + name: Building Python wheel for - ${{ matrix.settings.os }} - ${{ matrix.settings.target }} + runs-on: ${{ matrix.settings.os || 'ubuntu-latest' }} + needs: + - generate_schemas + - setup + env: + _PACKAGE_VERSION: ${{ needs.setup.outputs.package_version }} + strategy: + fail-fast: false + matrix: + settings: + - os: macos-12 + target: x86_64-apple-darwin + + - os: macos-12 + target: aarch64-apple-darwin + + - os: windows-2022 + target: x86_64-pc-windows-msvc + + - os: ubuntu-22.04 + target: x86_64-unknown-linux-gnu + + - os: ubuntu-22.04 + target: aarch64-unknown-linux-gnu + + steps: + - name: Checkout repo + uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 + + - name: Setup Node + uses: actions/setup-node@b39b52d1213e96004bfcb1c61a8a6fa8ab84f3e8 # v4.0.1 + with: + node-version: 18 + + - name: Install rust + uses: dtolnay/rust-toolchain@be73d7920c329f220ce78e0234b8f96b7ae60248 # stable + with: + toolchain: stable + targets: ${{ matrix.settings.target }} + + - name: Cache cargo registry + uses: Swatinem/rust-cache@23bce251a8cd2ffc3c1075eaa2367cf899916d84 # v2.7.3 + with: + key: ${{ matrix.settings.target }}-cargo-${{ matrix.settings.os }} + + - name: Retrieve schemas + uses: actions/download-artifact@6b208ae046db98c579e8a3aa621ab581ff575935 # v4.1.1 + with: + name: schemas.py + path: ${{ github.workspace }}/languages/python/bitwarden_sdk + + - name: Build wheels + if: ${{ matrix.settings.target != 'x86_64-unknown-linux-gnu' }} + uses: PyO3/maturin-action@a3013db91b2ef2e51420cfe99ee619c8e72a17e6 # v1.40.8 + with: + target: ${{ matrix.settings.target }} + args: --release --find-interpreter --sdist + sccache: "true" + manylinux: "2_28" # https://github.com/pola-rs/polars/pull/12211 + working-directory: ${{ github.workspace }}/languages/python + + - name: Build wheels (Linux - x86_64) + if: ${{ matrix.settings.target == 'x86_64-unknown-linux-gnu' }} + uses: PyO3/maturin-action@a3013db91b2ef2e51420cfe99ee619c8e72a17e6 # v1.40.8 + with: + target: ${{ matrix.settings.target }} + args: --release --find-interpreter --sdist + container: quay.io/pypa/manylinux_2_28_x86_64:2023-11-20-745eb52 + sccache: "true" + manylinux: "2_28" # https://github.com/pola-rs/polars/pull/12211 + working-directory: ${{ github.workspace }}/languages/python + + - name: Upload wheels + uses: actions/upload-artifact@a8a3f3ad30e3422c9c7b888a15615d19a852ae32 # v3.1.3 + with: + name: bitwarden_sdk-${{ env._PACKAGE_VERSION }}-${{ matrix.settings.target }} + path: ${{ github.workspace }}/target/wheels/bitwarden_sdk*.whl + + - name: Upload sdists + if: ${{ matrix.settings.target == 'x86_64-unknown-linux-gnu' }} # we only need one sdist + uses: actions/upload-artifact@a8a3f3ad30e3422c9c7b888a15615d19a852ae32 # v3.1.3 + with: + name: bitwarden_sdk-${{ env._PACKAGE_VERSION }}-sdist + path: ${{ github.workspace }}/target/wheels/bitwarden_sdk-*.tar.gz diff --git a/.github/workflows/build-rust-crates.yml b/.github/workflows/build-rust-crates.yml index b008540fb..5d45b6f95 100644 --- a/.github/workflows/build-rust-crates.yml +++ b/.github/workflows/build-rust-crates.yml @@ -31,6 +31,8 @@ jobs: - bitwarden - bitwarden-api-api - bitwarden-api-identity + - bitwarden-crypto + - bitwarden-generators steps: - name: Checkout @@ -43,7 +45,7 @@ jobs: targets: ${{ matrix.settings.target }} - name: Cache cargo registry - uses: Swatinem/rust-cache@3cf7f8cc28d1b4e7d01e3783be10a97d55d483c8 # v2.7.1 + uses: Swatinem/rust-cache@23bce251a8cd2ffc3c1075eaa2367cf899916d84 # v2.7.3 - name: Build run: cargo build -p ${{ matrix.package }} --release @@ -73,7 +75,7 @@ jobs: targets: ${{ matrix.settings.target }} - name: Cache cargo registry - uses: Swatinem/rust-cache@3cf7f8cc28d1b4e7d01e3783be10a97d55d483c8 # v2.7.1 + uses: Swatinem/rust-cache@23bce251a8cd2ffc3c1075eaa2367cf899916d84 # v2.7.3 - name: Install cargo-release run: cargo install cargo-release diff --git a/.github/workflows/build-rust-cross-platform.yml b/.github/workflows/build-rust-cross-platform.yml index 8999295a2..7495457df 100644 --- a/.github/workflows/build-rust-cross-platform.yml +++ b/.github/workflows/build-rust-cross-platform.yml @@ -30,7 +30,7 @@ jobs: toolchain: stable - name: Cache cargo registry - uses: Swatinem/rust-cache@3cf7f8cc28d1b4e7d01e3783be10a97d55d483c8 # v2.7.1 + uses: Swatinem/rust-cache@23bce251a8cd2ffc3c1075eaa2367cf899916d84 # v2.7.3 - name: Add build architecture run: rustup target add ${{ matrix.settings.target }} @@ -41,7 +41,7 @@ jobs: run: cargo build --target ${{ matrix.settings.target }} --release - name: Upload Artifact - uses: actions/upload-artifact@c7d193f32edcb7bfad88892161225aeda64e9392 # v4.0.0 + uses: actions/upload-artifact@694cdabd8bdb0f10b2cea11669e1bf5453eed0a6 # v4.2.0 with: name: libbitwarden_c_files-${{ matrix.settings.target }} path: | diff --git a/.github/workflows/direct-minimal-versions.yml b/.github/workflows/direct-minimal-versions.yml index eb911072f..8db6e3c2e 100644 --- a/.github/workflows/direct-minimal-versions.yml +++ b/.github/workflows/direct-minimal-versions.yml @@ -45,7 +45,7 @@ jobs: targets: ${{ matrix.settings.target }} - name: Cache cargo registry - uses: Swatinem/rust-cache@3cf7f8cc28d1b4e7d01e3783be10a97d55d483c8 # v2.7.1 + uses: Swatinem/rust-cache@23bce251a8cd2ffc3c1075eaa2367cf899916d84 # v2.7.3 with: key: dmv-${{ matrix.settings.target }}-cargo-${{ matrix.settings.os }} diff --git a/.github/workflows/generate_schemas.yml b/.github/workflows/generate_schemas.yml index 53bbf34e3..a18b4910a 100644 --- a/.github/workflows/generate_schemas.yml +++ b/.github/workflows/generate_schemas.yml @@ -31,54 +31,54 @@ jobs: run: npm ci - name: Cache cargo registry - uses: Swatinem/rust-cache@3cf7f8cc28d1b4e7d01e3783be10a97d55d483c8 # v2.7.1 + uses: Swatinem/rust-cache@23bce251a8cd2ffc3c1075eaa2367cf899916d84 # v2.7.3 - name: NPM Schemas run: npm run schemas - name: Upload ts schemas artifact - uses: actions/upload-artifact@c7d193f32edcb7bfad88892161225aeda64e9392 # v4.0.0 + uses: actions/upload-artifact@694cdabd8bdb0f10b2cea11669e1bf5453eed0a6 # v4.2.0 with: name: schemas.ts path: ${{ github.workspace }}/languages/js/sdk-client/src/schemas.ts if-no-files-found: error - name: Upload c# schemas artifact - uses: actions/upload-artifact@c7d193f32edcb7bfad88892161225aeda64e9392 # v4.0.0 + uses: actions/upload-artifact@694cdabd8bdb0f10b2cea11669e1bf5453eed0a6 # v4.2.0 with: name: schemas.cs path: ${{ github.workspace }}/languages/csharp/Bitwarden.Sdk/schemas.cs if-no-files-found: error - name: Upload python schemas artifact - uses: actions/upload-artifact@c7d193f32edcb7bfad88892161225aeda64e9392 # v4.0.0 + uses: actions/upload-artifact@694cdabd8bdb0f10b2cea11669e1bf5453eed0a6 # v4.2.0 with: name: 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@c7d193f32edcb7bfad88892161225aeda64e9392 # v4.0.0 + uses: actions/upload-artifact@694cdabd8bdb0f10b2cea11669e1bf5453eed0a6 # v4.2.0 with: name: schemas.rb path: ${{ github.workspace }}/languages/ruby/bitwarden_sdk/lib/schemas.rb if-no-files-found: error - name: Upload json schemas artifact - uses: actions/upload-artifact@c7d193f32edcb7bfad88892161225aeda64e9392 # v4.0.0 + uses: actions/upload-artifact@694cdabd8bdb0f10b2cea11669e1bf5453eed0a6 # v4.2.0 with: name: sdk-schemas-json path: ${{ github.workspace }}/support/schemas/* if-no-files-found: error - name: Upload Go schemas artifact - uses: actions/upload-artifact@c7d193f32edcb7bfad88892161225aeda64e9392 # v4.0.0 + uses: actions/upload-artifact@694cdabd8bdb0f10b2cea11669e1bf5453eed0a6 # v4.2.0 with: name: schemas.go path: ${{ github.workspace }}/languages/go/schema.go - name: Upload java schemas artifact - uses: actions/upload-artifact@c7d193f32edcb7bfad88892161225aeda64e9392 # v4.0.0 + uses: actions/upload-artifact@694cdabd8bdb0f10b2cea11669e1bf5453eed0a6 # v4.2.0 with: name: sdk-schemas-java path: ${{ github.workspace }}/languages/java/src/main/java/com/bitwarden/sdk/schema/* diff --git a/.github/workflows/golang-release.yml b/.github/workflows/golang-release.yml index f42d428f9..1578dee57 100644 --- a/.github/workflows/golang-release.yml +++ b/.github/workflows/golang-release.yml @@ -34,7 +34,7 @@ jobs: go-version: ${{ env.GO_VERSION }} - name: Cache dependencies - uses: actions/cache@704facf57e6136b1bc63b828d79edcd491f0ee84 # v3.3.2 + uses: actions/cache@e12d46a63a90f2fae62d114769bbf2a179198b5c # v3.3.3 with: path: ~/go/pkg/mod key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }} diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index f11ff4ced..2f8989f91 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -24,11 +24,16 @@ jobs: with: toolchain: stable + - name: Install rust nightly + run: | + rustup toolchain install nightly + rustup component add rustfmt --toolchain nightly-x86_64-unknown-linux-gnu + - name: Cache cargo registry - uses: Swatinem/rust-cache@3cf7f8cc28d1b4e7d01e3783be10a97d55d483c8 # v2.7.1 + uses: Swatinem/rust-cache@23bce251a8cd2ffc3c1075eaa2367cf899916d84 # v2.7.3 - name: Cargo fmt - run: cargo fmt --check + run: cargo +nightly fmt --check - name: Set up Node uses: actions/setup-node@b39b52d1213e96004bfcb1c61a8a6fa8ab84f3e8 # v4.0.1 diff --git a/.github/workflows/publish-dotnet.yml b/.github/workflows/publish-dotnet.yml index a7a6a6e0e..031a8b36f 100644 --- a/.github/workflows/publish-dotnet.yml +++ b/.github/workflows/publish-dotnet.yml @@ -26,7 +26,7 @@ jobs: uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 - name: Download C# schemas artifact - uses: actions/download-artifact@f44cd7b40bfd40b6aa1cc1b9b5b7bf03d3c67110 # v4.1.0 + uses: actions/download-artifact@6b208ae046db98c579e8a3aa621ab581ff575935 # v4.1.1 with: name: schemas.cs path: languages/csharp/Bitwarden.Sdk @@ -37,25 +37,25 @@ jobs: global-json-file: languages/csharp/global.json - name: Download x86_64-apple-darwin files - uses: actions/download-artifact@f44cd7b40bfd40b6aa1cc1b9b5b7bf03d3c67110 # v4.1.0 + uses: actions/download-artifact@6b208ae046db98c579e8a3aa621ab581ff575935 # v4.1.1 with: name: libbitwarden_c_files-x86_64-apple-darwin path: languages/csharp/Bitwarden.Sdk/macos-x64 - name: Download aarch64-apple-darwin files - uses: actions/download-artifact@f44cd7b40bfd40b6aa1cc1b9b5b7bf03d3c67110 # v4.1.0 + uses: actions/download-artifact@6b208ae046db98c579e8a3aa621ab581ff575935 # v4.1.1 with: name: libbitwarden_c_files-aarch64-apple-darwin path: languages/csharp/Bitwarden.Sdk/macos-arm64 - name: Download x86_64-unknown-linux-gnu files - uses: actions/download-artifact@f44cd7b40bfd40b6aa1cc1b9b5b7bf03d3c67110 # v4.1.0 + uses: actions/download-artifact@6b208ae046db98c579e8a3aa621ab581ff575935 # v4.1.1 with: name: libbitwarden_c_files-x86_64-unknown-linux-gnu - path: languages/csharp/Bitwarden.Sdk/ubuntu-x64 + path: languages/csharp/Bitwarden.Sdk/linux-x64 - name: Download x86_64-pc-windows-msvc files - uses: actions/download-artifact@f44cd7b40bfd40b6aa1cc1b9b5b7bf03d3c67110 # v4.1.0 + uses: actions/download-artifact@6b208ae046db98c579e8a3aa621ab581ff575935 # v4.1.1 with: name: libbitwarden_c_files-x86_64-pc-windows-msvc path: languages/csharp/Bitwarden.Sdk/windows-x64 diff --git a/.github/workflows/publish-php.yml b/.github/workflows/publish-php.yml index 6ed52537a..7406aae33 100644 --- a/.github/workflows/publish-php.yml +++ b/.github/workflows/publish-php.yml @@ -20,7 +20,7 @@ jobs: uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 - name: Setup PHP with PECL extension - uses: shivammathur/setup-php@e6f75134d35752277f093989e72e140eaa222f35 # 2.28.0 + uses: shivammathur/setup-php@6d7209f44a25a59e904b1ee9f3b0c33ab2cd888d # 2.29.0 with: php-version: "8.0" tools: composer @@ -33,25 +33,25 @@ jobs: working-directory: languages/php/ - name: Download x86_64-apple-darwin files - uses: actions/download-artifact@f44cd7b40bfd40b6aa1cc1b9b5b7bf03d3c67110 # v4.1.0 + uses: actions/download-artifact@6b208ae046db98c579e8a3aa621ab581ff575935 # v4.1.1 with: name: libbitwarden_c_files-x86_64-apple-darwin path: temp/macos-x64 - name: Download aarch64-apple-darwin files - uses: actions/download-artifact@f44cd7b40bfd40b6aa1cc1b9b5b7bf03d3c67110 # v4.1.0 + uses: actions/download-artifact@6b208ae046db98c579e8a3aa621ab581ff575935 # v4.1.1 with: name: libbitwarden_c_files-aarch64-apple-darwin path: temp/macos-arm64 - name: Download x86_64-unknown-linux-gnu files - uses: actions/download-artifact@f44cd7b40bfd40b6aa1cc1b9b5b7bf03d3c67110 # v4.1.0 + uses: actions/download-artifact@6b208ae046db98c579e8a3aa621ab581ff575935 # v4.1.1 with: name: libbitwarden_c_files-x86_64-unknown-linux-gnu - path: temp/ubuntu-x64 + path: temp/linux-x64 - name: Download x86_64-pc-windows-msvc files - uses: actions/download-artifact@f44cd7b40bfd40b6aa1cc1b9b5b7bf03d3c67110 # v4.1.0 + uses: actions/download-artifact@6b208ae046db98c579e8a3aa621ab581ff575935 # v4.1.1 with: name: libbitwarden_c_files-x86_64-pc-windows-msvc path: temp/windows-x64 @@ -59,11 +59,11 @@ jobs: - 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/linux-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") + platforms=("macos-arm64" "linux-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 diff --git a/.github/workflows/publish-python.yml b/.github/workflows/publish-python.yml new file mode 100644 index 000000000..bccde1aa2 --- /dev/null +++ b/.github/workflows/publish-python.yml @@ -0,0 +1,13 @@ +--- +name: Publish Python SDK + +on: + workflow_dispatch: + +jobs: + stub: + name: Stub + runs-on: ubuntu-22.04 + steps: + - name: Stub + run: echo "Stub" diff --git a/.github/workflows/publish-ruby.yml b/.github/workflows/publish-ruby.yml index 4315dea66..291834022 100644 --- a/.github/workflows/publish-ruby.yml +++ b/.github/workflows/publish-ruby.yml @@ -23,36 +23,36 @@ jobs: uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 - name: Set up Ruby - uses: ruby/setup-ruby@360dc864d5da99d54fcb8e9148c14a84b90d3e88 # v1.165.1 + uses: ruby/setup-ruby@5daca165445f0ae10478593083f72ca2625e241d # v1.169.0 with: ruby-version: 3.2 - name: Download Ruby schemas artifact - uses: actions/download-artifact@f44cd7b40bfd40b6aa1cc1b9b5b7bf03d3c67110 # v4.1.0 + uses: actions/download-artifact@6b208ae046db98c579e8a3aa621ab581ff575935 # v4.1.1 with: name: schemas.rb path: languages/ruby/bitwarden_sdk/lib - name: Download x86_64-apple-darwin files - uses: actions/download-artifact@f44cd7b40bfd40b6aa1cc1b9b5b7bf03d3c67110 # v4.1.0 + uses: actions/download-artifact@6b208ae046db98c579e8a3aa621ab581ff575935 # v4.1.1 with: name: libbitwarden_c_files-x86_64-apple-darwin path: temp/macos-x64 - name: Download aarch64-apple-darwin files - uses: actions/download-artifact@f44cd7b40bfd40b6aa1cc1b9b5b7bf03d3c67110 # v4.1.0 + uses: actions/download-artifact@6b208ae046db98c579e8a3aa621ab581ff575935 # v4.1.1 with: name: libbitwarden_c_files-aarch64-apple-darwin path: temp/macos-arm64 - name: Download x86_64-unknown-linux-gnu files - uses: actions/download-artifact@f44cd7b40bfd40b6aa1cc1b9b5b7bf03d3c67110 # v4.1.0 + uses: actions/download-artifact@6b208ae046db98c579e8a3aa621ab581ff575935 # v4.1.1 with: name: libbitwarden_c_files-x86_64-unknown-linux-gnu - path: temp/ubuntu-x64 + path: temp/linux-x64 - name: Download x86_64-pc-windows-msvc files - uses: actions/download-artifact@f44cd7b40bfd40b6aa1cc1b9b5b7bf03d3c67110 # v4.1.0 + uses: actions/download-artifact@6b208ae046db98c579e8a3aa621ab581ff575935 # v4.1.1 with: name: libbitwarden_c_files-x86_64-pc-windows-msvc path: temp/windows-x64 @@ -60,11 +60,11 @@ jobs: - 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/linux-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") + platforms=("macos-arm64" "linux-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 diff --git a/.github/workflows/publish-rust-crates.yml b/.github/workflows/publish-rust-crates.yml index aef94b37e..2f3ea6380 100644 --- a/.github/workflows/publish-rust-crates.yml +++ b/.github/workflows/publish-rust-crates.yml @@ -29,6 +29,16 @@ on: required: true default: true type: boolean + publish_bitwarden-crypto: + description: "Publish bitwarden-crypto crate" + required: true + default: true + type: boolean + publish_bitwarden-generators: + description: "Publish bitwarden-generators crate" + required: true + default: true + type: boolean defaults: run: @@ -61,6 +71,8 @@ jobs: PUBLISH_BITWARDEN: ${{ github.event.inputs.publish_bitwarden }} PUBLISH_BITWARDEN_API_API: ${{ github.event.inputs.publish_bitwarden-api-api }} PUBLISH_BITWARDEN_API_IDENTITY: ${{ github.event.inputs.publish_bitwarden-api-identity }} + PUBLISH_BITWARDEN_CRYPTO: ${{ github.event.inputs.publish_bitwarden-crypto }} + PUBLISH_BITWARDEN_GENERATORS: ${{ github.event.inputs.publish_bitwarden-generators }} run: | if [[ "$PUBLISH_BITWARDEN" == "false" ]] && [[ "$PUBLISH_BITWARDEN_API_API" == "false" ]] && [[ "$PUBLISH_BITWARDEN_API_IDENTITY" == "false" ]]; then echo "===================================" @@ -87,6 +99,16 @@ jobs: PACKAGES_LIST="$PACKAGES_LIST bitwarden-api-identity" fi + if [[ "$PUBLISH_BITWARDEN_CRYPTO" == "true" ]]; then + PACKAGES_COMMAND="$PACKAGES_COMMAND -p bitwarden-crypto" + PACKAGES_LIST="$PACKAGES_LIST bitwarden-crypto" + fi + + if [[ "$PUBLISH_BITWARDEN_GENERATORS" == "true" ]]; then + PACKAGES_COMMAND="$PACKAGES_COMMAND -p bitwarden-generators" + PACKAGES_LIST="$PACKAGES_LIST bitwarden-generators" + fi + echo "Packages command: " $PACKAGES_COMMAND echo "Packages list: " $PACKAGES_LIST @@ -103,7 +125,7 @@ jobs: uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 - name: Login to Azure - uses: Azure/login@de95379fe4dadc2defb305917eaa7e5dde727294 # v1.5.1 + uses: Azure/login@e15b166166a8746d1a47596803bd8c1b595455cf # v1.6.0 with: creds: ${{ secrets.AZURE_KV_CI_SERVICE_PRINCIPAL }} @@ -120,7 +142,7 @@ jobs: toolchain: stable - name: Cache cargo registry - uses: Swatinem/rust-cache@3cf7f8cc28d1b4e7d01e3783be10a97d55d483c8 # v2.7.1 + uses: Swatinem/rust-cache@23bce251a8cd2ffc3c1075eaa2367cf899916d84 # v2.7.3 - name: Install cargo-release run: cargo install cargo-release diff --git a/.github/workflows/release-cli.yml b/.github/workflows/release-cli.yml index ee08c2be8..a6059a1d5 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@de95379fe4dadc2defb305917eaa7e5dde727294 # v1.5.1 + uses: Azure/login@e15b166166a8746d1a47596803bd8c1b595455cf # v1.6.0 with: creds: ${{ secrets.AZURE_KV_CI_SERVICE_PRINCIPAL }} @@ -145,7 +145,7 @@ jobs: toolchain: stable - name: Cache cargo registry - uses: Swatinem/rust-cache@3cf7f8cc28d1b4e7d01e3783be10a97d55d483c8 # v2.7.1 + uses: Swatinem/rust-cache@23bce251a8cd2ffc3c1075eaa2367cf899916d84 # v2.7.3 - name: Install cargo-release run: cargo install cargo-release diff --git a/.github/workflows/release-napi.yml b/.github/workflows/release-napi.yml index 57498c681..02e29c527 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@de95379fe4dadc2defb305917eaa7e5dde727294 # v1.5.1 + uses: Azure/login@e15b166166a8746d1a47596803bd8c1b595455cf # v1.6.0 with: creds: ${{ secrets.AZURE_KV_CI_SERVICE_PRINCIPAL }} diff --git a/.github/workflows/rust-test.yml b/.github/workflows/rust-test.yml index ee8c55a22..8dfcf1783 100644 --- a/.github/workflows/rust-test.yml +++ b/.github/workflows/rust-test.yml @@ -44,7 +44,7 @@ jobs: toolchain: stable - name: Cache cargo registry - uses: Swatinem/rust-cache@3cf7f8cc28d1b4e7d01e3783be10a97d55d483c8 # v2.7.1 + uses: Swatinem/rust-cache@23bce251a8cd2ffc3c1075eaa2367cf899916d84 # v2.7.3 - name: Test run: cargo test --all-features @@ -64,7 +64,7 @@ jobs: components: llvm-tools - name: Cache cargo registry - uses: Swatinem/rust-cache@3cf7f8cc28d1b4e7d01e3783be10a97d55d483c8 # v2.7.1 + uses: Swatinem/rust-cache@23bce251a8cd2ffc3c1075eaa2367cf899916d84 # v2.7.3 - name: Install cargo-llvm-cov run: cargo install cargo-llvm-cov --version 0.5.38 @@ -93,7 +93,7 @@ jobs: targets: wasm32-unknown-unknown - name: Cache cargo registry - uses: Swatinem/rust-cache@3cf7f8cc28d1b4e7d01e3783be10a97d55d483c8 # v2.7.1 + uses: Swatinem/rust-cache@23bce251a8cd2ffc3c1075eaa2367cf899916d84 # v2.7.3 - name: Check run: cargo check -p bitwarden-wasm --target wasm32-unknown-unknown diff --git a/.github/workflows/version-bump.yml b/.github/workflows/version-bump.yml index b27a6a989..c3055f097 100644 --- a/.github/workflows/version-bump.yml +++ b/.github/workflows/version-bump.yml @@ -10,12 +10,14 @@ on: required: true type: choice options: - - napi - bitwarden - bitwarden-api-api - bitwarden-api-identity - - cli + - bitwarden-crypto + - bitwarden-generators - bitwarden-json + - cli + - napi version_number: description: "New version (example: '2024.1.0')" required: true @@ -35,13 +37,13 @@ jobs: toolchain: stable - name: Cache cargo registry - uses: Swatinem/rust-cache@3cf7f8cc28d1b4e7d01e3783be10a97d55d483c8 # v2.7.1 + uses: Swatinem/rust-cache@23bce251a8cd2ffc3c1075eaa2367cf899916d84 # v2.7.3 - name: Install cargo-release run: cargo install cargo-edit - name: Login to Azure - CI Subscription - uses: Azure/login@de95379fe4dadc2defb305917eaa7e5dde727294 # v1.5.1 + uses: Azure/login@e15b166166a8746d1a47596803bd8c1b595455cf # v1.6.0 with: creds: ${{ secrets.AZURE_KV_CI_SERVICE_PRINCIPAL }} @@ -116,6 +118,18 @@ jobs: if: ${{ inputs.project == 'bitwarden-api-identity' }} run: cargo-set-version set-version -p bitwarden-api-identity ${{ inputs.version_number }} + ### bitwarden-crypto + + - name: Bump bitwarden-crypto crate Version + if: ${{ inputs.project == 'bitwarden-crypto' }} + run: cargo-set-version set-version -p bitwarden-crypto ${{ inputs.version_number }} + + ### bitwarden-generators + + - name: Bump bitwarden-generators crate Version + if: ${{ inputs.project == 'bitwarden-generators' }} + run: cargo-set-version set-version -p bitwarden-generators ${{ inputs.version_number }} + ### cli - name: Bump cli Version diff --git a/.gitignore b/.gitignore index 23cbaba20..63c5875a3 100644 --- a/.gitignore +++ b/.gitignore @@ -50,8 +50,7 @@ languages/swift/BitwardenFFI.xcframework languages/swift/tmp languages/swift/.build languages/swift/.swiftpm -languages/kotlin/sdk/src/main/java/com/bitwarden/sdk/bitwarden_uniffi.kt -languages/kotlin/sdk/src/main/java/com/bitwarden/core/bitwarden.kt +languages/kotlin/sdk/src/main/java/com/bitwarden/**/*.kt # Schemas crates/bitwarden-napi/src-ts/bitwarden_client/schemas.ts diff --git a/Cargo.lock b/Cargo.lock index e04702e37..1fefd59fd 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -26,6 +26,7 @@ dependencies = [ "cfg-if", "cipher", "cpufeatures", + "zeroize", ] [[package]] @@ -63,9 +64,9 @@ dependencies = [ [[package]] name = "anstream" -version = "0.6.5" +version = "0.6.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d664a92ecae85fd0a7392615844904654d1d5f5514837f471ddef4a057aba1b6" +checksum = "6e2e1ebcb11de5c03c67de28a7df593d32191b44939c482e97702baaaa6ab6a5" dependencies = [ "anstyle", "anstyle-parse", @@ -123,14 +124,15 @@ checksum = "bddcadddf5e9015d310179a59bb28c4d4b9920ad0f11e8e14dbadf654890c9a6" [[package]] name = "argon2" -version = "0.5.2" +version = "0.5.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "17ba4cac0a46bc1d2912652a751c47f2a9f3a7fe89bcae2275d418f5270402f9" +checksum = "3c3610892ee6e0cbce8ae2700349fcf8f98adb0dbfbee85aec3c9179d29cc072" dependencies = [ "base64ct", "blake2", "cpufeatures", "password-hash", + "zeroize", ] [[package]] @@ -145,9 +147,9 @@ dependencies = [ [[package]] name = "askama_derive" -version = "0.12.2" +version = "0.12.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9a0fc7dcf8bd4ead96b1d36b41df47c14beedf7b0301fc543d8f2384e66a2ec0" +checksum = "19fe8d6cb13c4714962c072ea496f3392015f0989b1a2847bb4b2d9effd71d83" dependencies = [ "askama_parser", "basic-toml", @@ -156,7 +158,7 @@ dependencies = [ "proc-macro2", "quote", "serde", - "syn 2.0.47", + "syn 2.0.48", ] [[package]] @@ -167,9 +169,9 @@ checksum = "619743e34b5ba4e9703bba34deac3427c72507c7159f5fd030aea8cac0cfe341" [[package]] name = "askama_parser" -version = "0.1.1" +version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c268a96e01a4c47c8c5c2472aaa570707e006a875ea63e819f75474ceedaf7b4" +checksum = "acb1161c6b64d1c3d83108213c2a2533a342ac225aabd0bda218278c2ddb00c0" dependencies = [ "nom", ] @@ -210,24 +212,24 @@ dependencies = [ [[package]] name = "async-lock" -version = "3.2.0" +version = "3.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7125e42787d53db9dd54261812ef17e937c95a51e4d291373b670342fa44310c" +checksum = "d034b430882f8381900d3fe6f0aaa3ad94f2cb4ac519b429692a1bc2dda4ae7b" dependencies = [ - "event-listener 4.0.0", + "event-listener 4.0.3", "event-listener-strategy", "pin-project-lite", ] [[package]] name = "async-trait" -version = "0.1.74" +version = "0.1.77" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a66537f1bb974b254c98ed142ff995236e81b9d0fe4db0575f46612cb15eb0f9" +checksum = "c980ee35e870bd1a4d2c8294d4c04d0499e67bca1e4b5cefcc693c2fa00caea9" dependencies = [ "proc-macro2", "quote", - "syn 2.0.47", + "syn 2.0.48", ] [[package]] @@ -259,9 +261,9 @@ checksum = "9e1b586273c5702936fe7b7d6896644d8be71e6314cfe09d3167c95f712589e8" [[package]] name = "base64" -version = "0.21.5" +version = "0.21.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "35636a1494ede3b646cc98f74f8e62c773a38a659ebc777a2cf26b9b74171df9" +checksum = "9d297deb1925b89f2ccc13d7635fa0714f12c87adce1c75356b39ca9b7178567" [[package]] name = "base64ct" @@ -271,9 +273,9 @@ checksum = "8c3c1a368f70d6cf7302d78f8f7093da241fb8e8807c05cc9e51a125895a6d5b" [[package]] name = "basic-toml" -version = "0.1.7" +version = "0.1.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2f2139706359229bfa8f19142ac1155b4b80beafb7a60471ac5dd109d4a19778" +checksum = "2db21524cad41c5591204d22d75e1970a2d1f71060214ca931dc7d5afe2c14e5" dependencies = [ "serde", ] @@ -294,7 +296,7 @@ dependencies = [ "flate2", "globset", "home", - "nu-ansi-term 0.49.0", + "nu-ansi-term", "once_cell", "path_abs", "plist", @@ -338,34 +340,26 @@ checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" [[package]] name = "bitflags" -version = "2.4.1" +version = "2.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "327762f6e5a765692301e5bb513e0d9fef63be86bbc14528052b1cd3e6f03e07" +checksum = "ed570934406eb16438a4e976b1b4500774099c13b8cb96eec99f620f05090ddf" [[package]] name = "bitwarden" version = "0.4.0" dependencies = [ - "aes", - "argon2", - "base64 0.21.5", + "base64 0.21.7", "bitwarden-api-api", "bitwarden-api-identity", - "cbc", + "bitwarden-crypto", + "bitwarden-generators", "chrono", - "data-encoding", - "getrandom 0.2.11", - "hkdf", + "getrandom 0.2.12", "hmac", - "lazy_static", "log", - "num-bigint", - "num-traits", - "pbkdf2", "rand 0.8.5", "rand_chacha 0.3.1", "reqwest", - "rsa", "rustls-platform-verifier", "schemars", "serde", @@ -374,7 +368,6 @@ dependencies = [ "serde_repr", "sha1", "sha2", - "subtle", "thiserror", "tokio", "uniffi", @@ -428,6 +421,52 @@ dependencies = [ "supports-color", ] +[[package]] +name = "bitwarden-crypto" +version = "0.1.0" +dependencies = [ + "aes", + "argon2", + "base64 0.21.7", + "cbc", + "generic-array", + "hkdf", + "hmac", + "num-bigint", + "num-traits", + "pbkdf2", + "rand 0.8.5", + "rand_chacha 0.3.1", + "rsa", + "schemars", + "serde", + "serde_json", + "sha1", + "sha2", + "subtle", + "thiserror", + "uniffi", + "uuid", + "zeroize", +] + +[[package]] +name = "bitwarden-generators" +version = "0.1.0" +dependencies = [ + "bitwarden-crypto", + "rand 0.8.5", + "rand_chacha 0.3.1", + "reqwest", + "schemars", + "serde", + "serde_json", + "thiserror", + "tokio", + "uniffi", + "wiremock", +] + [[package]] name = "bitwarden-json" version = "0.3.0" @@ -469,6 +508,8 @@ version = "0.1.0" dependencies = [ "async-lock", "bitwarden", + "bitwarden-crypto", + "bitwarden-generators", "chrono", "env_logger", "schemars", @@ -519,9 +560,9 @@ dependencies = [ [[package]] name = "bstr" -version = "1.8.0" +version = "1.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "542f33a8835a0884b006a0c3df3dadd99c0c3f296ed26c2fdc8028e01ad6230c" +checksum = "c48f0051a4b4c5e0b6d365cd04af53aeaa209e3cc15ec2cdb69e73cc87fbd0dc" dependencies = [ "memchr", "serde", @@ -576,9 +617,9 @@ dependencies = [ [[package]] name = "bytemuck" -version = "1.14.0" +version = "1.14.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "374d28ec25809ee0e23827c2ab573d729e293f281dfe393500e7ad618baa61c6" +checksum = "ed2490600f404f2b94c167e31d3ed1d5f3c225a0f3b80230053b3e0b7b962bd9" [[package]] name = "byteorder" @@ -609,9 +650,9 @@ dependencies = [ [[package]] name = "cargo-platform" -version = "0.1.5" +version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e34637b3140142bdf929fb439e8aa4ebad7651ebf7b1080b3930aa16ac1459ff" +checksum = "ceed8ef69d8518a5dda55c07425450b58a4e1946f4951eab6d7191ee86c2443d" dependencies = [ "serde", ] @@ -662,9 +703,9 @@ checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" [[package]] name = "chrono" -version = "0.4.31" +version = "0.4.33" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7f2c685bad3eb3d45a01354cedb7d5faa66194d1d58ba6e267a8de788f79db38" +checksum = "9f13690e35a5e4ace198e7beea2895d29f3a9cc55015fcebe6336bd2010af9eb" dependencies = [ "android-tzdata", "iana-time-zone", @@ -672,7 +713,7 @@ dependencies = [ "num-traits", "serde", "wasm-bindgen", - "windows-targets 0.48.5", + "windows-targets 0.52.0", ] [[package]] @@ -683,13 +724,14 @@ checksum = "773f3b9af64447d2ce9850330c473515014aa235e6a783b02db81ff39e4a3dad" dependencies = [ "crypto-common", "inout", + "zeroize", ] [[package]] name = "clap" -version = "4.4.13" +version = "4.4.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "52bdc885e4cacc7f7c9eedc1ef6da641603180c783c41a15c264944deeaab642" +checksum = "1e578d6ec4194633722ccf9544794b71b1385c3c027efe0c55db226fc880865c" dependencies = [ "clap_builder", "clap_derive", @@ -697,9 +739,9 @@ dependencies = [ [[package]] name = "clap_builder" -version = "4.4.12" +version = "4.4.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fb7fb5e4e979aec3be7791562fcba452f94ad85e954da024396433e0e25a79e9" +checksum = "4df4df40ec50c46000231c914968278b1eb05098cf8f1b3a518a95030e71d1c7" dependencies = [ "anstream", "anstyle", @@ -709,9 +751,9 @@ dependencies = [ [[package]] name = "clap_complete" -version = "4.4.6" +version = "4.4.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "97aeaa95557bd02f23fbb662f981670c3d20c5a26e69f7354b28f57092437fcd" +checksum = "df631ae429f6613fcd3a7c1adbdb65f637271e561b03680adaa6573015dfb106" dependencies = [ "clap", ] @@ -725,7 +767,7 @@ dependencies = [ "heck", "proc-macro2", "quote", - "syn 2.0.47", + "syn 2.0.48", ] [[package]] @@ -812,15 +854,15 @@ dependencies = [ [[package]] name = "console" -version = "0.15.7" +version = "0.15.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c926e00cc70edefdc64d3a5ff31cc65bb97a3460097762bd23afb4d8145fccf8" +checksum = "0e1f83fc076bd6dd27517eacdf25fef6c4dfe5f1d7448bafaaf3a26f13b5e4eb" dependencies = [ "encode_unicode", "lazy_static", "libc", "unicode-width", - "windows-sys 0.45.0", + "windows-sys 0.52.0", ] [[package]] @@ -886,9 +928,9 @@ checksum = "06ea2b9bc92be3c2baa9334a323ebca2d6f074ff852cd1d7b11064035cd3868f" [[package]] name = "cpufeatures" -version = "0.2.11" +version = "0.2.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ce420fe07aecd3e67c5f910618fe65e94158f6dcc0adf44e00d69ce2bdfe0fd0" +checksum = "53fe5e26ff1b7aef8bca9c6080520cfb8d9333c7568e1829cef191a9723e5504" dependencies = [ "libc", ] @@ -904,12 +946,9 @@ dependencies = [ [[package]] name = "crossbeam-utils" -version = "0.8.17" +version = "0.8.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c06d96137f14f244c37f989d9fff8f95e6c18b918e71f36638f8c49112e4c78f" -dependencies = [ - "cfg-if", -] +checksum = "248e3bacc7dc6baa3b21e405ee045c3047101a49145e7e9eca583ab4c2ca5345" [[package]] name = "crossterm" @@ -933,7 +972,7 @@ version = "0.27.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f476fe445d41c9e991fd07515a6f463074b782242ccf4a5b7b1d1012e70824df" dependencies = [ - "bitflags 2.4.1", + "bitflags 2.4.2", "crossterm_winapi", "libc", "parking_lot", @@ -966,7 +1005,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "30d2b3721e861707777e3195b0158f950ae6dc4a27e4d02ff9f67e3eb3de199e" dependencies = [ "quote", - "syn 2.0.47", + "syn 2.0.48", ] [[package]] @@ -1004,12 +1043,6 @@ dependencies = [ "syn 1.0.109", ] -[[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" @@ -1042,9 +1075,9 @@ dependencies = [ [[package]] name = "deranged" -version = "0.3.10" +version = "0.3.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8eb30d70a07a3b04884d2677f06bec33509dc67ca60d92949e5535352d3191dc" +checksum = "b42b6fa04a440b495c8b04d0e71b707c585f83cb9cb28cf8cd0d976c315e31b4" dependencies = [ "powerfmt", ] @@ -1142,9 +1175,9 @@ dependencies = [ [[package]] name = "env_logger" -version = "0.10.1" +version = "0.10.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "95b3f3e67048839cb0d0781f445682a35113da7121f7c949db0e2be96a4fbece" +checksum = "4cd405aab171cb85d6735e5c8d9db038c17d3ca007a4d2c25f337935c3d90580" dependencies = [ "humantime", "is-terminal", @@ -1177,9 +1210,9 @@ checksum = "0206175f82b8d6bf6652ff7d71a1e27fd2e4efde587fd368662814d6ec1d9ce0" [[package]] name = "event-listener" -version = "4.0.0" +version = "4.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "770d968249b5d99410d61f5bf89057f3199a077a04d087092f58e7d10692baae" +checksum = "67b215c49b2b248c855fb73579eb1f4f26c38ffdc12973e20e07b91d78d5646e" dependencies = [ "concurrent-queue", "parking", @@ -1192,7 +1225,7 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "958e4d70b6d5e81971bebec42271ec641e7ff4e170a6fa605f2b8a8b65cb97d3" dependencies = [ - "event-listener 4.0.0", + "event-listener 4.0.3", "pin-project-lite", ] @@ -1267,9 +1300,9 @@ dependencies = [ [[package]] name = "futures" -version = "0.3.29" +version = "0.3.30" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "da0290714b38af9b4a7b094b8a37086d1b4e61f2df9122c3cad2577669145335" +checksum = "645c6916888f6cb6350d2550b80fb63e734897a8498abe35cfb732b6487804b0" dependencies = [ "futures-channel", "futures-core", @@ -1282,9 +1315,9 @@ dependencies = [ [[package]] name = "futures-channel" -version = "0.3.29" +version = "0.3.30" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ff4dd66668b557604244583e3e1e1eada8c5c2e96a6d0d6653ede395b78bbacb" +checksum = "eac8f7d7865dcb88bd4373ab671c8cf4508703796caa2b1985a9ca867b3fcb78" dependencies = [ "futures-core", "futures-sink", @@ -1292,15 +1325,15 @@ dependencies = [ [[package]] name = "futures-core" -version = "0.3.29" +version = "0.3.30" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eb1d22c66e66d9d72e1758f0bd7d4fd0bee04cad842ee34587d68c07e45d088c" +checksum = "dfc6580bb841c5a68e9ef15c77ccc837b40a7504914d52e47b8b0e9bbda25a1d" [[package]] name = "futures-executor" -version = "0.3.29" +version = "0.3.30" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0f4fb8693db0cf099eadcca0efe2a5a22e4550f98ed16aba6c48700da29597bc" +checksum = "a576fc72ae164fca6b9db127eaa9a9dda0d61316034f33a0a0d4eda41f02b01d" dependencies = [ "futures-core", "futures-task", @@ -1309,9 +1342,9 @@ dependencies = [ [[package]] name = "futures-io" -version = "0.3.29" +version = "0.3.30" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8bf34a163b5c4c52d0478a4d757da8fb65cabef42ba90515efee0f6f9fa45aaa" +checksum = "a44623e20b9681a318efdd71c299b6b222ed6f231972bfe2f224ebad6311f0c1" [[package]] name = "futures-lite" @@ -1330,26 +1363,26 @@ dependencies = [ [[package]] name = "futures-macro" -version = "0.3.29" +version = "0.3.30" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "53b153fd91e4b0147f4aced87be237c98248656bb01050b96bf3ee89220a8ddb" +checksum = "87750cf4b7a4c0625b1529e4c543c2182106e4dedc60a2a6455e00d212c489ac" dependencies = [ "proc-macro2", "quote", - "syn 2.0.47", + "syn 2.0.48", ] [[package]] name = "futures-sink" -version = "0.3.29" +version = "0.3.30" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e36d3378ee38c2a36ad710c5d30c2911d752cb941c00c72dbabfb786a7970817" +checksum = "9fb8e00e87438d937621c1c6269e53f536c14d3fbd6a042bb24879e57d474fb5" [[package]] name = "futures-task" -version = "0.3.29" +version = "0.3.30" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "efd193069b0ddadc69c46389b740bbccdd97203899b48d09c5f7969591d6bae2" +checksum = "38d84fa142264698cdce1a9f9172cf383a0c82de1bddcf3092901442c4097004" [[package]] name = "futures-timer" @@ -1359,9 +1392,9 @@ checksum = "e64b03909df88034c26dc1547e8970b91f98bdb65165d6a4e9110d94263dbb2c" [[package]] name = "futures-util" -version = "0.3.29" +version = "0.3.30" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a19526d624e703a3179b3d322efec918b6246ea0fa51d41124525f00f1cc8104" +checksum = "3d6401deb83407ab3da39eba7e33987a73c3df0c82b4bb5813ee871c19c41d48" dependencies = [ "futures-channel", "futures-core", @@ -1375,19 +1408,6 @@ dependencies = [ "slab", ] -[[package]] -name = "generator" -version = "0.7.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5cc16584ff22b460a382b7feec54b23d2908d858152e5739a120b949293bd74e" -dependencies = [ - "cc", - "libc", - "log", - "rustversion", - "windows", -] - [[package]] name = "generic-array" version = "0.14.7" @@ -1396,6 +1416,7 @@ checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" dependencies = [ "typenum", "version_check", + "zeroize", ] [[package]] @@ -1411,9 +1432,9 @@ dependencies = [ [[package]] name = "getrandom" -version = "0.2.11" +version = "0.2.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fe9006bed769170c11f845cf00c7c1e9092aeb3f268e007c3e760ac68008070f" +checksum = "190092ea657667030ac6a35e305e62fc4dd69fd98ac98631e5d3a2b1575a12b5" dependencies = [ "cfg-if", "js-sys", @@ -1443,15 +1464,15 @@ dependencies = [ "aho-corasick", "bstr", "log", - "regex-automata 0.4.3", + "regex-automata", "regex-syntax 0.8.2", ] [[package]] name = "goblin" -version = "0.6.1" +version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0d6b4de4a8eb6c46a8c77e1d3be942cb9a8bf073c22374578e5ba4b08ed0ff68" +checksum = "bb07a4ffed2093b118a525b1d8f5204ae274faed5604537caf7135d0f18d9887" dependencies = [ "log", "plain", @@ -1460,9 +1481,9 @@ dependencies = [ [[package]] name = "h2" -version = "0.3.22" +version = "0.3.24" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4d6250322ef6e60f93f9a2162799302cd6f68f79f6e5d85c8c16f14d1d958178" +checksum = "bb2c4422095b67ee78da96fbb51a4cc413b3b25883c7717ff7ca1ab31022c9c9" dependencies = [ "bytes", "fnv", @@ -1497,9 +1518,9 @@ checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8" [[package]] name = "hermit-abi" -version = "0.3.3" +version = "0.3.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d77f7ec81a6d05a3abb01ab6eb7590f6083d08449fe5a1c8b1e620283546ccb7" +checksum = "5d3d0e0f38255e7fa3cf31335b3a56f05febd18025f4db5ef7a0cfb4f8da651f" [[package]] name = "hkdf" @@ -1591,9 +1612,9 @@ checksum = "9a3a5bfb195931eeb336b2a7b4d761daec841b97f947d34394601737a7bba5e4" [[package]] name = "hyper" -version = "0.14.27" +version = "0.14.28" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ffb1cfd654a8219eaef89881fdb3bb3b1cdc5fa75ded05d6933b2b382e395468" +checksum = "bf96e135eb83a2a8ddf766e426a841d8ddd7449d5f00d34ea02b41d2f19eef80" dependencies = [ "bytes", "futures-channel", @@ -1606,7 +1627,7 @@ dependencies = [ "httpdate", "itoa", "pin-project-lite", - "socket2 0.4.10", + "socket2", "tokio", "tower-service", "tracing", @@ -1629,9 +1650,9 @@ dependencies = [ [[package]] name = "iana-time-zone" -version = "0.1.58" +version = "0.1.59" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8326b86b6cff230b97d0d312a6c40a60726df3332e721f72a1b035f451663b20" +checksum = "b6a67363e2aa4443928ce15e57ebae94fd8949958fd1223c4cfc0cd473ad7539" dependencies = [ "android_system_properties", "core-foundation-sys", @@ -1748,13 +1769,13 @@ checksum = "8f518f335dce6725a761382244631d86cf0ccb2863413590b31338feb467f9c3" [[package]] name = "is-terminal" -version = "0.4.9" +version = "0.4.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cb0889898416213fab133e1d33a0e5858a48177452750691bde3666d0fdbaf8b" +checksum = "0bad00257d07be169d870ab665980b06cdb366d792ad690bf2e76876dc503455" dependencies = [ "hermit-abi", "rustix", - "windows-sys 0.48.0", + "windows-sys 0.52.0", ] [[package]] @@ -1809,9 +1830,9 @@ checksum = "8eaf4bc02d17cbdd7ff4c7438cafcdf7fb9a4613313ad11b4f8fefe7d3fa0130" [[package]] name = "js-sys" -version = "0.3.66" +version = "0.3.67" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cee9c64da59eae3b50095c18d3e74f8b73c0b86d2792824ff01bbce68ba229ca" +checksum = "9a1d36f1235bc969acba30b7f5990b864423a6068a10f7c90ae8f0112e3a59d1" dependencies = [ "wasm-bindgen", ] @@ -1827,9 +1848,9 @@ dependencies = [ [[package]] name = "libc" -version = "0.2.151" +version = "0.2.152" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "302d7ab3130588088d277783b1e2d2e10c9e9e4a16dd9050e6ec93fb3e7048f4" +checksum = "13e3bf6590cbc649f4d1a3eefc9d5d6eb746f5200ffb04e5e142700b8faa56e7" [[package]] name = "libloading" @@ -1853,7 +1874,7 @@ version = "0.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "85c833ca1e66078851dba29046874e38f08b2c883700aa29a03ddd3b23814ee8" dependencies = [ - "bitflags 2.4.1", + "bitflags 2.4.2", "libc", "redox_syscall", ] @@ -1869,9 +1890,9 @@ dependencies = [ [[package]] name = "linux-raw-sys" -version = "0.4.12" +version = "0.4.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c4cd1a83af159aa67994778be9070f0ae1bd732942279cabb14f86f986a21456" +checksum = "01cda141df6706de531b6c46c3a33ecca755538219bd484262fa09410c13539c" [[package]] name = "lock_api" @@ -1889,34 +1910,11 @@ version = "0.4.20" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b5e6163cb8c49088c2c36f57875e58ccd8c87c7427f7fbd50ea6710b2f3f2e8f" -[[package]] -name = "loom" -version = "0.5.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ff50ecb28bb86013e935fb6683ab1f6d3a20016f123c76fd4c27470076ac30f5" -dependencies = [ - "cfg-if", - "generator", - "pin-utils", - "scoped-tls", - "tracing", - "tracing-subscriber", -] - -[[package]] -name = "matchers" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8263075bb86c5a1b1427b5ae862e8889656f126e9f77c484496e8b47cf5c5558" -dependencies = [ - "regex-automata 0.1.10", -] - [[package]] name = "memchr" -version = "2.6.4" +version = "2.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f665ee40bc4a3c5590afb1e9677db74a508659dfd71e126420da8274909a0167" +checksum = "523dc4f511e55ab87b694dc30d0f820d60906ef06413f93d4d7a1385599cc149" [[package]] name = "memoffset" @@ -1972,11 +1970,11 @@ dependencies = [ [[package]] name = "napi" -version = "2.14.1" +version = "2.14.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1133249c46e92da921bafc8aba4912bf84d6c475f7625183772ed2d0844dc3a7" +checksum = "902495f6b80f53f8435aefbbd2241c9c675fa239cd7e5f8e28fb57f3b69ecd09" dependencies = [ - "bitflags 2.4.1", + "bitflags 2.4.2", "ctor", "napi-derive", "napi-sys", @@ -1992,23 +1990,23 @@ checksum = "d4b4532cf86bfef556348ac65e561e3123879f0e7566cca6d43a6ff5326f13df" [[package]] name = "napi-derive" -version = "2.14.4" +version = "2.14.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9b5af262f1d8e660742eb722abc7113a5b3c3de4144d0ef23ede2518672ceff1" +checksum = "e61bec1ee990ae3e9a5f443484c65fb38e571a898437f0ad283ed69c82fc59c0" dependencies = [ "cfg-if", "convert_case", "napi-derive-backend", "proc-macro2", "quote", - "syn 2.0.47", + "syn 2.0.48", ] [[package]] name = "napi-derive-backend" -version = "1.0.57" +version = "1.0.58" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4ea236321b521d6926213a2021e407b0562e28a257c037a45919e414d2cdb4f8" +checksum = "2314f777bc9cde51705d991c44466cee4de4a3f41c6d3d019fcbbebb5cdd47c4" dependencies = [ "convert_case", "once_cell", @@ -2016,7 +2014,7 @@ dependencies = [ "quote", "regex", "semver", - "syn 2.0.47", + "syn 2.0.48", ] [[package]] @@ -2047,16 +2045,6 @@ dependencies = [ "minimal-lexical", ] -[[package]] -name = "nu-ansi-term" -version = "0.46.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "77a8165726e8236064dbb45459242600304b42a5ea24ee2948e18e023bf7ba84" -dependencies = [ - "overload", - "winapi", -] - [[package]] name = "nu-ansi-term" version = "0.49.0" @@ -2137,9 +2125,9 @@ dependencies = [ [[package]] name = "object" -version = "0.32.1" +version = "0.32.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9cf5f9dd3933bd50a9e1f149ec995f39ae2c496d31fd772c1fd45ebc27e902b0" +checksum = "a6a622008b6e321afc04970976f62ee297fdbaa6f95318ca343e3eebb9648441" dependencies = [ "memchr", ] @@ -2151,13 +2139,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92" [[package]] -name = "oneshot" +name = "oneshot-uniffi" version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6f6640c6bda7731b1fdbab747981a0f896dd1fedaf9f4a53fa237a04a84431f4" -dependencies = [ - "loom", -] +checksum = "6c548d5c78976f6955d72d0ced18c48ca07030f7a1d4024529fedd7c1c01b29c" [[package]] name = "onig" @@ -2193,12 +2178,6 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d" -[[package]] -name = "overload" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b15813163c1d831bf4a13c3610c05c0d03b39feb07f7e09fa234dac9b15aaf39" - [[package]] name = "owo-colors" version = "3.5.0" @@ -2319,9 +2298,9 @@ dependencies = [ [[package]] name = "pkg-config" -version = "0.3.27" +version = "0.3.29" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "26072860ba924cbfa98ea39c8c19b4dd6a4a25423dbdf219c1eca91aa0cf6964" +checksum = "2900ede94e305130c13ddd391e0ab7cbaeb783945ae07a279c268cb05109c6cb" [[package]] name = "plain" @@ -2335,7 +2314,7 @@ version = "1.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e5699cc8a63d1aa2b1ee8e12b9ad70ac790d65788cd36101fa37f87ea46c4cef" dependencies = [ - "base64 0.21.5", + "base64 0.21.7", "indexmap 2.1.0", "line-wrap", "quick-xml", @@ -2357,9 +2336,9 @@ checksum = "5b40af805b3121feab8a3c29f04d8ad262fa8e0561883e7653e024ae4479e6de" [[package]] name = "proc-macro2" -version = "1.0.75" +version = "1.0.78" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "907a61bd0f64c2f29cd1cf1dc34d05176426a3f504a78010f08416ddb7b13708" +checksum = "e2422ad645d89c99f8f3e6b88a9fdeca7fabeac836b1002371c4367c8f984aae" dependencies = [ "unicode-ident", ] @@ -2446,7 +2425,7 @@ dependencies = [ "proc-macro2", "pyo3-macros-backend", "quote", - "syn 2.0.47", + "syn 2.0.48", ] [[package]] @@ -2458,7 +2437,7 @@ dependencies = [ "heck", "proc-macro2", "quote", - "syn 2.0.47", + "syn 2.0.48", ] [[package]] @@ -2544,7 +2523,7 @@ version = "0.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" dependencies = [ - "getrandom 0.2.11", + "getrandom 0.2.12", ] [[package]] @@ -2571,49 +2550,34 @@ version = "0.4.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a18479200779601e498ada4e8c1e1f50e3ee19deb0259c25825a98b5603b2cb4" dependencies = [ - "getrandom 0.2.11", + "getrandom 0.2.12", "libredox", "thiserror", ] [[package]] name = "regex" -version = "1.10.2" +version = "1.10.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "380b951a9c5e80ddfd6136919eef32310721aa4aacd4889a8d39124b026ab343" +checksum = "b62dbe01f0b06f9d8dc7d49e05a0785f153b00b2c227856282f671e0318c9b15" dependencies = [ "aho-corasick", "memchr", - "regex-automata 0.4.3", + "regex-automata", "regex-syntax 0.8.2", ] [[package]] name = "regex-automata" -version = "0.1.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6c230d73fb8d8c1b9c0b3135c5142a8acee3a0558fb8db5cf1cb65f8d7862132" -dependencies = [ - "regex-syntax 0.6.29", -] - -[[package]] -name = "regex-automata" -version = "0.4.3" +version = "0.4.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5f804c7828047e88b2d32e2d7fe5a105da8ee3264f01902f796c8e067dc2483f" +checksum = "3b7fa1134405e2ec9353fd416b17f8dacd46c473d7d3fd1cf202706a14eb792a" dependencies = [ "aho-corasick", "memchr", "regex-syntax 0.8.2", ] -[[package]] -name = "regex-syntax" -version = "0.6.29" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f162c6dd7b008981e4d40210aca20b4bd0f9b60ca9271061b07f78537722f2e1" - [[package]] name = "regex-syntax" version = "0.7.5" @@ -2628,11 +2592,11 @@ checksum = "c08c74e62047bb2de4ff487b251e4a92e24f48745648451635cec7d591162d9f" [[package]] name = "reqwest" -version = "0.11.22" +version = "0.11.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "046cd98826c46c2ac8ddecae268eb5c2e58628688a5fc7a2643704a73faba95b" +checksum = "37b1ae8d9ac08420c66222fb9096fc5de435c3c48542bc5336c51892cffafb41" dependencies = [ - "base64 0.21.5", + "base64 0.21.7", "bytes", "encoding_rs", "futures-core", @@ -2689,7 +2653,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "688c63d65483050968b2a8937f7995f443e27041a0f7700aa59b0822aedebb74" dependencies = [ "cc", - "getrandom 0.2.11", + "getrandom 0.2.12", "libc", "spin 0.9.8", "untrusted", @@ -2724,11 +2688,11 @@ checksum = "d626bb9dae77e28219937af045c257c28bfd3f69333c512553507f5f9798cb76" [[package]] name = "rustix" -version = "0.38.28" +version = "0.38.30" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "72e572a5e8ca657d7366229cdde4bd14c4eb5499a9573d4d366fe1b599daa316" +checksum = "322394588aaf33c24007e8bb3238ee3e4c5c09c084ab32bc73890b99ff326bca" dependencies = [ - "bitflags 2.4.1", + "bitflags 2.4.2", "errno", "libc", "linux-raw-sys", @@ -2765,14 +2729,14 @@ version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1c74cae0a4cf6ccbbf5f359f08efdf8ee7e1dc532573bf0db71968cb56b1448c" dependencies = [ - "base64 0.21.5", + "base64 0.21.7", ] [[package]] name = "rustls-platform-verifier" -version = "0.1.0" +version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c573e165e19be8c9fc0264ee041d66292d4ee0439949af61d71fa747bf5df082" +checksum = "92c57b5de012da34087f2fe711fa29770f9a7abdde660b01bac3c9dbdee91b84" dependencies = [ "core-foundation", "core-foundation-sys", @@ -2834,11 +2798,11 @@ dependencies = [ [[package]] name = "schannel" -version = "0.1.22" +version = "0.1.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0c3733bf4cf7ea0880754e19cb5a462007c4a8c1914bff372ccc95b464f1df88" +checksum = "fbc91545643bcf3a0bbb6569265615222618bdf33ce4ffbbd13c4bbd4c093534" dependencies = [ - "windows-sys 0.48.0", + "windows-sys 0.52.0", ] [[package]] @@ -2882,22 +2846,22 @@ checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" [[package]] name = "scroll" -version = "0.11.0" +version = "0.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "04c565b551bafbef4157586fa379538366e4385d42082f255bfd96e4fe8519da" +checksum = "6ab8598aa408498679922eff7fa985c25d58a90771bd6be794434c5277eab1a6" dependencies = [ "scroll_derive", ] [[package]] name = "scroll_derive" -version = "0.11.1" +version = "0.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1db149f81d46d2deba7cd3c50772474707729550221e69588478ebf9ada425ae" +checksum = "7f81c2fde025af7e69b1d1420531c8a8811ca898919db177141a85313b1cb932" dependencies = [ "proc-macro2", "quote", - "syn 2.0.47", + "syn 2.0.48", ] [[package]] @@ -2949,9 +2913,9 @@ dependencies = [ [[package]] name = "semver" -version = "1.0.20" +version = "1.0.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "836fa6a3e1e547f9a2c4040802ec865b5d85f4014efe00555d7090a3dcaa1090" +checksum = "b97ed7a9823b74f99c7742f5336af7be5ecd3eeafcb1507d1fa93347b1d589b0" dependencies = [ "serde", ] @@ -2973,7 +2937,7 @@ checksum = "46fe8f8603d81ba86327b23a2e9cdf49e1255fb94a4c5f297f6ee0547178ea2c" dependencies = [ "proc-macro2", "quote", - "syn 2.0.47", + "syn 2.0.48", ] [[package]] @@ -3022,20 +2986,20 @@ dependencies = [ [[package]] name = "serde_repr" -version = "0.1.17" +version = "0.1.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3081f5ffbb02284dda55132aa26daecedd7372a42417bbbab6f14ab7d6bb9145" +checksum = "0b2e6b945e9d3df726b65d6ee24060aff8e3533d431f677a9695db04eff9dfdb" dependencies = [ "proc-macro2", "quote", - "syn 2.0.47", + "syn 2.0.48", ] [[package]] name = "serde_spanned" -version = "0.6.4" +version = "0.6.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "12022b835073e5b11e90a14f86838ceb1c8fb0325b72416845c487ac0fa95e80" +checksum = "eb3622f419d1296904700073ea6cc23ad690adbd66f13ea683df73298736f0c1" dependencies = [ "serde", ] @@ -3054,9 +3018,9 @@ dependencies = [ [[package]] name = "serde_yaml" -version = "0.9.27" +version = "0.9.30" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3cc7a1570e38322cfe4154732e5110f887ea57e22b76f4bfd32b5bdd3368666c" +checksum = "b1bf28c79a99f70ee1f1d83d10c875d2e70618417fda01ad1785e027579d9d38" dependencies = [ "indexmap 2.1.0", "itoa", @@ -3153,9 +3117,9 @@ dependencies = [ [[package]] name = "smallvec" -version = "1.11.2" +version = "1.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4dccd0940a2dcdf68d092b8cbab7dc0ad8fa938bf95787e1b916b0e3d0e8e970" +checksum = "e6ecd384b10a64542d77071bd64bd7b231f4ed5940fba55e98c3de13824cf3d7" [[package]] name = "smawk" @@ -3163,16 +3127,6 @@ version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b7c388c1b5e93756d0c740965c41e8822f866621d41acbdf6336a6a168f8840c" -[[package]] -name = "socket2" -version = "0.4.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9f7916fc008ca5542385b89a3d3ce689953c143e9304a9bf8beec1de48994c0d" -dependencies = [ - "libc", - "winapi", -] - [[package]] name = "socket2" version = "0.5.5" @@ -3239,7 +3193,7 @@ dependencies = [ "proc-macro2", "quote", "rustversion", - "syn 2.0.47", + "syn 2.0.48", ] [[package]] @@ -3271,9 +3225,9 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.47" +version = "2.0.48" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1726efe18f42ae774cc644f330953a5e7b3c3003d3edcecf18850fe9d4dd9afb" +checksum = "0f3531638e407dfc0814761abb7c00a5b54992b849452a0646b7f65c9f770f3f" dependencies = [ "proc-macro2", "quote", @@ -3322,9 +3276,9 @@ dependencies = [ [[package]] name = "target-lexicon" -version = "0.12.12" +version = "0.12.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "14c39fd04924ca3a864207c66fc2cd7d22d7c016007f9ce846cbb9326331930a" +checksum = "69758bda2e78f098e4ccb393021a0963bb3442eac05f135c30f61b7370bbafae" [[package]] name = "tempfile" @@ -3341,9 +3295,9 @@ dependencies = [ [[package]] name = "termcolor" -version = "1.4.0" +version = "1.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ff1bc3d3f05aff0403e8ac0d92ced918ec05b666a43f83297ccef5bea8a3d449" +checksum = "06794f8f6c5c898b3275aebefa6b8a1cb24cd2c6c79397ab15774837a0bc5755" dependencies = [ "winapi-util", ] @@ -3376,7 +3330,7 @@ checksum = "fa0faa943b50f3db30a20aa7e265dbc66076993efed8463e8de414e5d06d3471" dependencies = [ "proc-macro2", "quote", - "syn 2.0.47", + "syn 2.0.48", ] [[package]] @@ -3391,9 +3345,9 @@ dependencies = [ [[package]] name = "time" -version = "0.3.30" +version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c4a34ab300f2dee6e562c10a046fc05e358b29f9bf92277f30c3c8d82275f6f5" +checksum = "f657ba42c3f86e7680e53c8cd3af8abbe56b5491790b46e22e19c0d57463583e" dependencies = [ "deranged", "itoa", @@ -3411,9 +3365,9 @@ checksum = "ef927ca75afb808a4d64dd374f00a2adf8d0fcff8e7b184af886c3c87ec4a3f3" [[package]] name = "time-macros" -version = "0.2.15" +version = "0.2.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4ad70d68dba9e1f8aceda7aa6711965dfec1cac869f311a51bd08b3a2ccbce20" +checksum = "26197e33420244aeb70c3e8c78376ca46571bc4e701e4791c2cd9f57dcb3a43f" dependencies = [ "time-core", ] @@ -3445,7 +3399,7 @@ dependencies = [ "mio", "num_cpus", "pin-project-lite", - "socket2 0.5.5", + "socket2", "tokio-macros", "windows-sys 0.48.0", ] @@ -3458,7 +3412,7 @@ checksum = "5b8a1e28f2deaa14e508979454cb3a223b10b938b45af148bc0986de36f1923b" dependencies = [ "proc-macro2", "quote", - "syn 2.0.47", + "syn 2.0.48", ] [[package]] @@ -3541,21 +3495,9 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c3523ab5a71916ccf420eebdf5521fcef02141234bbc0b8a49f2fdc4544364ef" dependencies = [ "pin-project-lite", - "tracing-attributes", "tracing-core", ] -[[package]] -name = "tracing-attributes" -version = "0.1.27" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "34704c8d6ebcbc939824180af020566b01a7c01f80641264eba0999f6c2b6be7" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.47", -] - [[package]] name = "tracing-core" version = "0.1.32" @@ -3576,33 +3518,15 @@ dependencies = [ "tracing-subscriber", ] -[[package]] -name = "tracing-log" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ee855f1f400bd0e5c02d150ae5de3840039a3f54b025156404e34c23c03f47c3" -dependencies = [ - "log", - "once_cell", - "tracing-core", -] - [[package]] name = "tracing-subscriber" version = "0.3.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ad0f048c97dbd9faa9b7df56362b8ebcaa52adb06b498c050d2f4e32f90a7a8b" dependencies = [ - "matchers", - "nu-ansi-term 0.46.0", - "once_cell", - "regex", "sharded-slab", - "smallvec", "thread_local", - "tracing", "tracing-core", - "tracing-log", ] [[package]] @@ -3628,9 +3552,9 @@ dependencies = [ [[package]] name = "unicode-bidi" -version = "0.3.14" +version = "0.3.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6f2528f27a9eb2b21e69c95319b30bd0efd85d09c379741b0f78ea1d86be2416" +checksum = "08f95100a766bf4f8f28f90d77e0a5461bbdb219042e7679bebe79004fed8d75" [[package]] name = "unicode-ident" @@ -3667,8 +3591,9 @@ checksum = "e51733f11c9c4f72aa0c160008246859e340b00807569a0da0e7a1079b27ba85" [[package]] name = "uniffi" -version = "0.25.2" -source = "git+https://github.com/mozilla/uniffi-rs?rev=23711c8151bbb794369aa1f9d383db386792dff9#23711c8151bbb794369aa1f9d383db386792dff9" +version = "0.26.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5ad0be8bba6c242d2d16922de4a9c8f167b9491729fda552e70f8626bf7302cb" dependencies = [ "anyhow", "camino", @@ -3688,8 +3613,9 @@ dependencies = [ [[package]] name = "uniffi_bindgen" -version = "0.25.2" -source = "git+https://github.com/mozilla/uniffi-rs?rev=23711c8151bbb794369aa1f9d383db386792dff9#23711c8151bbb794369aa1f9d383db386792dff9" +version = "0.26.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ab31006ab9c9c6870739f0e74235729d1478d82e73571b8f53c25aa176d67535" dependencies = [ "anyhow", "askama", @@ -3712,8 +3638,9 @@ dependencies = [ [[package]] name = "uniffi_build" -version = "0.25.2" -source = "git+https://github.com/mozilla/uniffi-rs?rev=23711c8151bbb794369aa1f9d383db386792dff9#23711c8151bbb794369aa1f9d383db386792dff9" +version = "0.26.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4aa3a7608c6872dc1ce53199d816a24d2e19af952d82ce557ecc8692a4ae9cba" dependencies = [ "anyhow", "camino", @@ -3722,17 +3649,19 @@ dependencies = [ [[package]] name = "uniffi_checksum_derive" -version = "0.25.2" -source = "git+https://github.com/mozilla/uniffi-rs?rev=23711c8151bbb794369aa1f9d383db386792dff9#23711c8151bbb794369aa1f9d383db386792dff9" +version = "0.26.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72775b3afa6adb30e0c92b3107858d2fcb0ff1a417ac242db1f648b0e2dd0ef2" dependencies = [ "quote", - "syn 2.0.47", + "syn 2.0.48", ] [[package]] name = "uniffi_core" -version = "0.25.2" -source = "git+https://github.com/mozilla/uniffi-rs?rev=23711c8151bbb794369aa1f9d383db386792dff9#23711c8151bbb794369aa1f9d383db386792dff9" +version = "0.26.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8d6e8db3f4e558faf0e25ac4b5bd775567973a4e18809f1123e74de52a853692" dependencies = [ "anyhow", "async-compat", @@ -3740,15 +3669,16 @@ dependencies = [ "camino", "log", "once_cell", - "oneshot", + "oneshot-uniffi", "paste", "static_assertions", ] [[package]] name = "uniffi_macros" -version = "0.25.2" -source = "git+https://github.com/mozilla/uniffi-rs?rev=23711c8151bbb794369aa1f9d383db386792dff9#23711c8151bbb794369aa1f9d383db386792dff9" +version = "0.26.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a126650799f97d97d8e38e3f10f15c65f5bc5a76b021bec21823efe9dd831a02" dependencies = [ "bincode", "camino", @@ -3757,7 +3687,7 @@ dependencies = [ "proc-macro2", "quote", "serde", - "syn 2.0.47", + "syn 2.0.48", "toml 0.5.11", "uniffi_build", "uniffi_meta", @@ -3765,8 +3695,9 @@ dependencies = [ [[package]] name = "uniffi_meta" -version = "0.25.2" -source = "git+https://github.com/mozilla/uniffi-rs?rev=23711c8151bbb794369aa1f9d383db386792dff9#23711c8151bbb794369aa1f9d383db386792dff9" +version = "0.26.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f64a99e905671738d9d293f9cce58708ce1af8e13ea29f9d6b6925114fc2e85" dependencies = [ "anyhow", "bytes", @@ -3776,8 +3707,9 @@ dependencies = [ [[package]] name = "uniffi_testing" -version = "0.25.2" -source = "git+https://github.com/mozilla/uniffi-rs?rev=23711c8151bbb794369aa1f9d383db386792dff9#23711c8151bbb794369aa1f9d383db386792dff9" +version = "0.26.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cdca5719a22edf34c8239cc6ac9e3906d7ebc2a3e8a5e6ece4c3dffc312a4251" dependencies = [ "anyhow", "camino", @@ -3788,8 +3720,9 @@ dependencies = [ [[package]] name = "uniffi_udl" -version = "0.25.2" -source = "git+https://github.com/mozilla/uniffi-rs?rev=23711c8151bbb794369aa1f9d383db386792dff9#23711c8151bbb794369aa1f9d383db386792dff9" +version = "0.26.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f6817c15714acccd0d0459f99b524cabebfdd622376464a2c6466a6485bdb4b" dependencies = [ "anyhow", "textwrap", @@ -3836,9 +3769,9 @@ checksum = "711b9620af191e0cdc7468a8d14e709c3dcdb115b36f838e601583af800a370a" [[package]] name = "uuid" -version = "1.6.1" +version = "1.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5e395fcf16a7a3d8127ec99782007af141946b4795001f876d54fb0d55978560" +checksum = "f00cc9702ca12d3c81455259621e676d0f7251cec66a21e98fe2e9a37db93b2a" dependencies = [ "serde", ] @@ -3894,9 +3827,9 @@ checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" [[package]] name = "wasm-bindgen" -version = "0.2.89" +version = "0.2.90" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0ed0d4f68a3015cc185aff4db9506a015f4b96f95303897bfa23f846db54064e" +checksum = "b1223296a201415c7fad14792dbefaace9bd52b62d33453ade1c5b5f07555406" dependencies = [ "cfg-if", "serde", @@ -3906,24 +3839,24 @@ dependencies = [ [[package]] name = "wasm-bindgen-backend" -version = "0.2.89" +version = "0.2.90" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1b56f625e64f3a1084ded111c4d5f477df9f8c92df113852fa5a374dbda78826" +checksum = "fcdc935b63408d58a32f8cc9738a0bffd8f05cc7c002086c6ef20b7312ad9dcd" dependencies = [ "bumpalo", "log", "once_cell", "proc-macro2", "quote", - "syn 2.0.47", + "syn 2.0.48", "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-futures" -version = "0.4.39" +version = "0.4.40" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ac36a15a220124ac510204aec1c3e5db8a22ab06fd6706d881dc6149f8ed9a12" +checksum = "bde2032aeb86bdfaecc8b261eef3cba735cc426c1f3a3416d1e0791be95fc461" dependencies = [ "cfg-if", "js-sys", @@ -3933,9 +3866,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro" -version = "0.2.89" +version = "0.2.90" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0162dbf37223cd2afce98f3d0785506dcb8d266223983e4b5b525859e6e182b2" +checksum = "3e4c238561b2d428924c49815533a8b9121c664599558a5d9ec51f8a1740a999" dependencies = [ "quote", "wasm-bindgen-macro-support", @@ -3943,28 +3876,28 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.89" +version = "0.2.90" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f0eb82fcb7930ae6219a7ecfd55b217f5f0893484b7a13022ebb2b2bf20b5283" +checksum = "bae1abb6806dc1ad9e560ed242107c0f6c84335f1749dd4e8ddb012ebd5e25a7" dependencies = [ "proc-macro2", "quote", - "syn 2.0.47", + "syn 2.0.48", "wasm-bindgen-backend", "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-shared" -version = "0.2.89" +version = "0.2.90" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7ab9b36309365056cd639da3134bf87fa8f3d86008abf99e612384a6eecd459f" +checksum = "4d91413b1c31d7539ba5ef2451af3f0b833a005eb27a631cec32bc0635a8602b" [[package]] name = "wasm-bindgen-test" -version = "0.3.39" +version = "0.3.40" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2cf9242c0d27999b831eae4767b2a146feb0b27d332d553e605864acd2afd403" +checksum = "139bd73305d50e1c1c4333210c0db43d989395b64a237bd35c10ef3832a7f70c" dependencies = [ "console_error_panic_hook", "js-sys", @@ -3976,20 +3909,20 @@ dependencies = [ [[package]] name = "wasm-bindgen-test-macro" -version = "0.3.39" +version = "0.3.40" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "794645f5408c9a039fd09f4d113cdfb2e7eba5ff1956b07bcf701cf4b394fe89" +checksum = "70072aebfe5da66d2716002c729a14e4aec4da0e23cc2ea66323dac541c93928" dependencies = [ "proc-macro2", "quote", - "syn 2.0.47", + "syn 2.0.48", ] [[package]] name = "web-sys" -version = "0.3.66" +version = "0.3.67" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "50c24a44ec86bb68fbecd1b3efed7e85ea5621b39b35ef2766b66cd984f8010f" +checksum = "58cd2333b6e0be7a39605f0e255892fd7418a682d8da8fe042fe25128794d2ed" dependencies = [ "js-sys", "wasm-bindgen", @@ -4003,8 +3936,9 @@ checksum = "1778a42e8b3b90bff8d0f5032bf22250792889a5cdc752aa0020c84abe3aaf10" [[package]] name = "weedle2" -version = "4.0.0" -source = "git+https://github.com/mozilla/uniffi-rs?rev=23711c8151bbb794369aa1f9d383db386792dff9#23711c8151bbb794369aa1f9d383db386792dff9" +version = "5.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "998d2c24ec099a87daf9467808859f9d82b61f1d9c9701251aea037f514eae0e" dependencies = [ "nom", ] @@ -4040,31 +3974,13 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" -[[package]] -name = "windows" -version = "0.48.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e686886bc078bc1b0b600cac0147aadb815089b6e4da64016cbd754b6342700f" -dependencies = [ - "windows-targets 0.48.5", -] - [[package]] name = "windows-core" -version = "0.51.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f1f8cf84f35d2db49a46868f947758c7a1138116f7fac3bc844f43ade1292e64" -dependencies = [ - "windows-targets 0.48.5", -] - -[[package]] -name = "windows-sys" -version = "0.45.0" +version = "0.52.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "75283be5efb2831d37ea142365f009c02ec203cd29a3ebecbc093d52315b66d0" +checksum = "33ab640c8d7e35bf8ba19b884ba838ceb4fba93a4e8c65a9059d08afcfc683d9" dependencies = [ - "windows-targets 0.42.2", + "windows-targets 0.52.0", ] [[package]] @@ -4085,21 +4001,6 @@ dependencies = [ "windows-targets 0.52.0", ] -[[package]] -name = "windows-targets" -version = "0.42.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e5180c00cd44c9b1c88adb3693291f1cd93605ded80c250a75d472756b4d071" -dependencies = [ - "windows_aarch64_gnullvm 0.42.2", - "windows_aarch64_msvc 0.42.2", - "windows_i686_gnu 0.42.2", - "windows_i686_msvc 0.42.2", - "windows_x86_64_gnu 0.42.2", - "windows_x86_64_gnullvm 0.42.2", - "windows_x86_64_msvc 0.42.2", -] - [[package]] name = "windows-targets" version = "0.48.5" @@ -4130,12 +4031,6 @@ dependencies = [ "windows_x86_64_msvc 0.52.0", ] -[[package]] -name = "windows_aarch64_gnullvm" -version = "0.42.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "597a5118570b68bc08d8d59125332c54f1ba9d9adeedeef5b99b02ba2b0698f8" - [[package]] name = "windows_aarch64_gnullvm" version = "0.48.5" @@ -4148,12 +4043,6 @@ 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" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e08e8864a60f06ef0d0ff4ba04124db8b0fb3be5776a5cd47641e942e58c4d43" - [[package]] name = "windows_aarch64_msvc" version = "0.48.5" @@ -4166,12 +4055,6 @@ 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" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c61d927d8da41da96a81f029489353e68739737d3beca43145c8afec9a31a84f" - [[package]] name = "windows_i686_gnu" version = "0.48.5" @@ -4184,12 +4067,6 @@ 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" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "44d840b6ec649f480a41c8d80f9c65108b92d89345dd94027bfe06ac444d1060" - [[package]] name = "windows_i686_msvc" version = "0.48.5" @@ -4202,12 +4079,6 @@ 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" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8de912b8b8feb55c064867cf047dda097f92d51efad5b491dfb98f6bbb70cb36" - [[package]] name = "windows_x86_64_gnu" version = "0.48.5" @@ -4220,12 +4091,6 @@ 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" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "26d41b46a36d453748aedef1486d5c7a85db22e56aff34643984ea85514e94a3" - [[package]] name = "windows_x86_64_gnullvm" version = "0.48.5" @@ -4238,12 +4103,6 @@ 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" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9aec5da331524158c6d1a4ac0ab1541149c0b9505fde06423b02f5ef0106b9f0" - [[package]] name = "windows_x86_64_msvc" version = "0.48.5" @@ -4258,9 +4117,9 @@ checksum = "dff9641d1cd4be8d1a070daf9e3773c5f67e78b4d9d42263020c057706765c04" [[package]] name = "winnow" -version = "0.5.28" +version = "0.5.34" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6c830786f7720c2fd27a1a0e27a709dbd3c4d009b56d098fc742d4f4eab91fe2" +checksum = "b7cf47b659b318dccbd69cc4797a39ae128f533dce7902a1096044d1967b9c16" dependencies = [ "memchr", ] @@ -4283,7 +4142,7 @@ checksum = "13a3a53eaf34f390dd30d7b1b078287dd05df2aa2e21a589ccb80f5c7253c2e9" dependencies = [ "assert-json-diff", "async-trait", - "base64 0.21.5", + "base64 0.21.7", "deadpool", "futures", "futures-timer", @@ -4302,6 +4161,20 @@ name = "zeroize" version = "1.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "525b4ec142c6b68a2d10f01f7bbf6755599ca3f81ea53b8431b7dd348f5fdb2d" +dependencies = [ + "zeroize_derive", +] + +[[package]] +name = "zeroize_derive" +version = "1.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce36e65b0d2999d2aafac989fb249189a141aee1f53c612c1f37d72631959f69" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.48", +] [[package]] name = "zxcvbn" diff --git a/Cargo.toml b/Cargo.toml index f841f07c9..125aac2a8 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -21,11 +21,3 @@ codegen-units = 1 # Stripping the binary reduces the size by ~30%, but the stacktraces won't be usable anymore. # This is fine as long as we don't have any unhandled panics, but let's keep it disabled for now # strip = true - -# 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 = "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/crates/bitwarden-api-api/src/apis/access_policies_api.rs b/crates/bitwarden-api-api/src/apis/access_policies_api.rs index 7e538c0b4..9eb40719d 100644 --- a/crates/bitwarden-api-api/src/apis/access_policies_api.rs +++ b/crates/bitwarden-api-api/src/apis/access_policies_api.rs @@ -27,21 +27,24 @@ pub enum AccessPoliciesIdPutError { UnknownValue(serde_json::Value), } -/// struct for typed errors of method [`organizations_id_access_policies_people_potential_grantees_get`] +/// struct for typed errors of method +/// [`organizations_id_access_policies_people_potential_grantees_get`] #[derive(Debug, Clone, Serialize, Deserialize)] #[serde(untagged)] pub enum OrganizationsIdAccessPoliciesPeoplePotentialGranteesGetError { UnknownValue(serde_json::Value), } -/// struct for typed errors of method [`organizations_id_access_policies_projects_potential_grantees_get`] +/// struct for typed errors of method +/// [`organizations_id_access_policies_projects_potential_grantees_get`] #[derive(Debug, Clone, Serialize, Deserialize)] #[serde(untagged)] pub enum OrganizationsIdAccessPoliciesProjectsPotentialGranteesGetError { UnknownValue(serde_json::Value), } -/// struct for typed errors of method [`organizations_id_access_policies_service_accounts_potential_grantees_get`] +/// struct for typed errors of method +/// [`organizations_id_access_policies_service_accounts_potential_grantees_get`] #[derive(Debug, Clone, Serialize, Deserialize)] #[serde(untagged)] pub enum OrganizationsIdAccessPoliciesServiceAccountsPotentialGranteesGetError { diff --git a/crates/bitwarden-api-api/src/apis/collections_api.rs b/crates/bitwarden-api-api/src/apis/collections_api.rs index 02ac6eb16..c95ac5529 100644 --- a/crates/bitwarden-api-api/src/apis/collections_api.rs +++ b/crates/bitwarden-api-api/src/apis/collections_api.rs @@ -62,7 +62,8 @@ pub enum OrganizationsOrgIdCollectionsIdDeletePostError { UnknownValue(serde_json::Value), } -/// struct for typed errors of method [`organizations_org_id_collections_id_delete_user_org_user_id_post`] +/// struct for typed errors of method +/// [`organizations_org_id_collections_id_delete_user_org_user_id_post`] #[derive(Debug, Clone, Serialize, Deserialize)] #[serde(untagged)] pub enum OrganizationsOrgIdCollectionsIdDeleteUserOrgUserIdPostError { @@ -97,7 +98,8 @@ pub enum OrganizationsOrgIdCollectionsIdPutError { UnknownValue(serde_json::Value), } -/// struct for typed errors of method [`organizations_org_id_collections_id_user_org_user_id_delete`] +/// struct for typed errors of method +/// [`organizations_org_id_collections_id_user_org_user_id_delete`] #[derive(Debug, Clone, Serialize, Deserialize)] #[serde(untagged)] pub enum OrganizationsOrgIdCollectionsIdUserOrgUserIdDeleteError { diff --git a/crates/bitwarden-api-api/src/apis/groups_api.rs b/crates/bitwarden-api-api/src/apis/groups_api.rs index 46ed9afa5..d52fcd200 100644 --- a/crates/bitwarden-api-api/src/apis/groups_api.rs +++ b/crates/bitwarden-api-api/src/apis/groups_api.rs @@ -48,7 +48,8 @@ pub enum OrganizationsOrgIdGroupsIdDeletePostError { UnknownValue(serde_json::Value), } -/// struct for typed errors of method [`organizations_org_id_groups_id_delete_user_org_user_id_post`] +/// struct for typed errors of method +/// [`organizations_org_id_groups_id_delete_user_org_user_id_post`] #[derive(Debug, Clone, Serialize, Deserialize)] #[serde(untagged)] pub enum OrganizationsOrgIdGroupsIdDeleteUserOrgUserIdPostError { diff --git a/crates/bitwarden-api-api/src/apis/organization_connections_api.rs b/crates/bitwarden-api-api/src/apis/organization_connections_api.rs index 8bf41ffd1..ee75716dc 100644 --- a/crates/bitwarden-api-api/src/apis/organization_connections_api.rs +++ b/crates/bitwarden-api-api/src/apis/organization_connections_api.rs @@ -20,14 +20,16 @@ pub enum OrganizationsConnectionsEnabledGetError { UnknownValue(serde_json::Value), } -/// struct for typed errors of method [`organizations_connections_organization_connection_id_delete`] +/// struct for typed errors of method +/// [`organizations_connections_organization_connection_id_delete`] #[derive(Debug, Clone, Serialize, Deserialize)] #[serde(untagged)] pub enum OrganizationsConnectionsOrganizationConnectionIdDeleteError { UnknownValue(serde_json::Value), } -/// struct for typed errors of method [`organizations_connections_organization_connection_id_delete_post`] +/// struct for typed errors of method +/// [`organizations_connections_organization_connection_id_delete_post`] #[derive(Debug, Clone, Serialize, Deserialize)] #[serde(untagged)] pub enum OrganizationsConnectionsOrganizationConnectionIdDeletePostError { diff --git a/crates/bitwarden-api-api/src/apis/organization_sponsorships_api.rs b/crates/bitwarden-api-api/src/apis/organization_sponsorships_api.rs index 203174fdf..e3f1ccf24 100644 --- a/crates/bitwarden-api-api/src/apis/organization_sponsorships_api.rs +++ b/crates/bitwarden-api-api/src/apis/organization_sponsorships_api.rs @@ -27,21 +27,24 @@ pub enum OrganizationSponsorshipSponsoredSponsoredOrgIdDeleteError { UnknownValue(serde_json::Value), } -/// struct for typed errors of method [`organization_sponsorship_sponsored_sponsored_org_id_remove_post`] +/// struct for typed errors of method +/// [`organization_sponsorship_sponsored_sponsored_org_id_remove_post`] #[derive(Debug, Clone, Serialize, Deserialize)] #[serde(untagged)] pub enum OrganizationSponsorshipSponsoredSponsoredOrgIdRemovePostError { UnknownValue(serde_json::Value), } -/// struct for typed errors of method [`organization_sponsorship_sponsoring_org_id_families_for_enterprise_post`] +/// struct for typed errors of method +/// [`organization_sponsorship_sponsoring_org_id_families_for_enterprise_post`] #[derive(Debug, Clone, Serialize, Deserialize)] #[serde(untagged)] pub enum OrganizationSponsorshipSponsoringOrgIdFamiliesForEnterprisePostError { UnknownValue(serde_json::Value), } -/// struct for typed errors of method [`organization_sponsorship_sponsoring_org_id_families_for_enterprise_resend_post`] +/// struct for typed errors of method +/// [`organization_sponsorship_sponsoring_org_id_families_for_enterprise_resend_post`] #[derive(Debug, Clone, Serialize, Deserialize)] #[serde(untagged)] pub enum OrganizationSponsorshipSponsoringOrgIdFamiliesForEnterpriseResendPostError { @@ -62,7 +65,8 @@ pub enum OrganizationSponsorshipSponsoringOrganizationIdDeleteError { UnknownValue(serde_json::Value), } -/// struct for typed errors of method [`organization_sponsorship_sponsoring_organization_id_delete_post`] +/// struct for typed errors of method +/// [`organization_sponsorship_sponsoring_organization_id_delete_post`] #[derive(Debug, Clone, Serialize, Deserialize)] #[serde(untagged)] pub enum OrganizationSponsorshipSponsoringOrganizationIdDeletePostError { 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 5a8e99916..804a29019 100644 --- a/crates/bitwarden-api-api/src/apis/organization_users_api.rs +++ b/crates/bitwarden-api-api/src/apis/organization_users_api.rs @@ -174,14 +174,16 @@ pub enum OrganizationsOrgIdUsersInvitePostError { UnknownValue(serde_json::Value), } -/// struct for typed errors of method [`organizations_org_id_users_organization_user_id_accept_init_post`] +/// struct for typed errors of method +/// [`organizations_org_id_users_organization_user_id_accept_init_post`] #[derive(Debug, Clone, Serialize, Deserialize)] #[serde(untagged)] pub enum OrganizationsOrgIdUsersOrganizationUserIdAcceptInitPostError { UnknownValue(serde_json::Value), } -/// struct for typed errors of method [`organizations_org_id_users_organization_user_id_accept_post`] +/// struct for typed errors of method +/// [`organizations_org_id_users_organization_user_id_accept_post`] #[derive(Debug, Clone, Serialize, Deserialize)] #[serde(untagged)] pub enum OrganizationsOrgIdUsersOrganizationUserIdAcceptPostError { @@ -230,7 +232,8 @@ pub enum OrganizationsOrgIdUsersRevokePutError { UnknownValue(serde_json::Value), } -/// struct for typed errors of method [`organizations_org_id_users_user_id_reset_password_enrollment_put`] +/// struct for typed errors of method +/// [`organizations_org_id_users_user_id_reset_password_enrollment_put`] #[derive(Debug, Clone, Serialize, Deserialize)] #[serde(untagged)] pub enum OrganizationsOrgIdUsersUserIdResetPasswordEnrollmentPutError { diff --git a/crates/bitwarden-api-api/src/apis/self_hosted_organization_sponsorships_api.rs b/crates/bitwarden-api-api/src/apis/self_hosted_organization_sponsorships_api.rs index dc2da4103..60b8a6d28 100644 --- a/crates/bitwarden-api-api/src/apis/self_hosted_organization_sponsorships_api.rs +++ b/crates/bitwarden-api-api/src/apis/self_hosted_organization_sponsorships_api.rs @@ -13,21 +13,24 @@ use reqwest; use super::{configuration, Error}; use crate::apis::ResponseContent; -/// struct for typed errors of method [`organization_sponsorship_self_hosted_sponsoring_org_id_delete`] +/// struct for typed errors of method +/// [`organization_sponsorship_self_hosted_sponsoring_org_id_delete`] #[derive(Debug, Clone, Serialize, Deserialize)] #[serde(untagged)] pub enum OrganizationSponsorshipSelfHostedSponsoringOrgIdDeleteError { UnknownValue(serde_json::Value), } -/// struct for typed errors of method [`organization_sponsorship_self_hosted_sponsoring_org_id_delete_post`] +/// struct for typed errors of method +/// [`organization_sponsorship_self_hosted_sponsoring_org_id_delete_post`] #[derive(Debug, Clone, Serialize, Deserialize)] #[serde(untagged)] pub enum OrganizationSponsorshipSelfHostedSponsoringOrgIdDeletePostError { UnknownValue(serde_json::Value), } -/// struct for typed errors of method [`organization_sponsorship_self_hosted_sponsoring_org_id_families_for_enterprise_post`] +/// struct for typed errors of method +/// [`organization_sponsorship_self_hosted_sponsoring_org_id_families_for_enterprise_post`] #[derive(Debug, Clone, Serialize, Deserialize)] #[serde(untagged)] pub enum OrganizationSponsorshipSelfHostedSponsoringOrgIdFamiliesForEnterprisePostError { diff --git a/crates/bitwarden-api-api/src/models/sso_configuration_data.rs b/crates/bitwarden-api-api/src/models/sso_configuration_data.rs index 0e2890ad9..2d898488e 100644 --- a/crates/bitwarden-api-api/src/models/sso_configuration_data.rs +++ b/crates/bitwarden-api-api/src/models/sso_configuration_data.rs @@ -17,7 +17,10 @@ pub struct SsoConfigurationData { skip_serializing_if = "Option::is_none" )] pub member_decryption_type: Option, - /// Legacy property to determine if KeyConnector was enabled. Kept for backwards compatibility with old configs that will not have the new Bit.Core.Auth.Models.Data.SsoConfigurationData.MemberDecryptionType when deserialized from the database. + /// Legacy property to determine if KeyConnector was enabled. Kept for backwards compatibility + /// with old configs that will not have the new + /// Bit.Core.Auth.Models.Data.SsoConfigurationData.MemberDecryptionType when deserialized from + /// the database. #[serde( rename = "keyConnectorEnabled", skip_serializing_if = "Option::is_none" diff --git a/crates/bitwarden-cli/Cargo.toml b/crates/bitwarden-cli/Cargo.toml index 217ac88c1..bded30904 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.13", features = ["derive"] } +clap = { version = "4.4.18", features = ["derive"] } color-eyre = "0.6" inquire = "0.6.2" supports-color = "2.1.0" diff --git a/crates/bitwarden-crypto/Cargo.toml b/crates/bitwarden-crypto/Cargo.toml new file mode 100644 index 000000000..295cdd38e --- /dev/null +++ b/crates/bitwarden-crypto/Cargo.toml @@ -0,0 +1,48 @@ +[package] +name = "bitwarden-crypto" +version = "0.1.0" +authors = ["Bitwarden Inc"] +license-file = "LICENSE" +repository = "https://github.com/bitwarden/sdk" +homepage = "https://bitwarden.com" +description = """ +Internal crate for the bitwarden crate. Do not use. +""" +keywords = ["bitwarden"] +edition = "2021" +rust-version = "1.57" + +[features] +default = [] + +mobile = ["uniffi"] + +[dependencies] +aes = { version = ">=0.8.2, <0.9", features = ["zeroize"] } +argon2 = { version = ">=0.5.0, <0.6", features = [ + "alloc", + "zeroize", +], default-features = false } +base64 = ">=0.21.2, <0.22" +cbc = { version = ">=0.1.2, <0.2", features = ["alloc", "zeroize"] } +generic-array = { version = ">=0.14.7, <1.0", features = ["zeroize"] } +hkdf = ">=0.12.3, <0.13" +hmac = ">=0.12.1, <0.13" +num-bigint = ">=0.4, <0.5" +num-traits = ">=0.2.15, <0.3" +pbkdf2 = { version = ">=0.12.1, <0.13", default-features = false } +rand = ">=0.8.5, <0.9" +rsa = ">=0.9.2, <0.10" +schemars = { version = ">=0.8, <0.9", features = ["uuid1"] } +serde = { version = ">=1.0, <2.0", features = ["derive"] } +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.26.1", optional = true } +uuid = { version = ">=1.3.3, <2.0", features = ["serde"] } +zeroize = { version = ">=1.7.0, <2.0", features = ["derive", "aarch64"] } + +[dev-dependencies] +rand_chacha = "0.3.1" +serde_json = ">=1.0.96, <2.0" diff --git a/crates/bitwarden-crypto/README.md b/crates/bitwarden-crypto/README.md new file mode 100644 index 000000000..fd697aa3c --- /dev/null +++ b/crates/bitwarden-crypto/README.md @@ -0,0 +1,6 @@ +# Bitwarden Crypto + +This is an internal crate for the Bitwarden SDK do not depend on this directly and use the +[`bitwarden`](https://crates.io/crates/bitwarden) crate instead. + +This crate does not follow semantic versioning and the public interface may change at any time. diff --git a/crates/bitwarden/src/crypto/aes_ops.rs b/crates/bitwarden-crypto/src/aes.rs similarity index 75% rename from crates/bitwarden/src/crypto/aes_ops.rs rename to crates/bitwarden-crypto/src/aes.rs index 00e71a3fd..195b818b7 100644 --- a/crates/bitwarden/src/crypto/aes_ops.rs +++ b/crates/bitwarden-crypto/src/aes.rs @@ -2,36 +2,38 @@ //! //! Contains low level AES operations used by the rest of the library. //! -//! **Warning**: Consider carefully if you have to use these functions directly, as generally we -//! expose higher level functions that are easier to use and more secure. -//! -//! In most cases you should use the [EncString] with [KeyEncryptable][super::KeyEncryptable] & -//! [KeyDecryptable][super::KeyDecryptable] instead. +//! In most cases you should use the [EncString][crate::EncString] with +//! [KeyEncryptable][crate::KeyEncryptable] & [KeyDecryptable][crate::KeyDecryptable] instead. use aes::cipher::{ - block_padding::Pkcs7, generic_array::GenericArray, typenum::U32, BlockDecryptMut, - BlockEncryptMut, KeyIvInit, + block_padding::Pkcs7, typenum::U32, BlockDecryptMut, BlockEncryptMut, KeyIvInit, }; +use generic_array::GenericArray; use hmac::Mac; use subtle::ConstantTimeEq; use crate::{ - crypto::{PbkdfSha256Hmac, PBKDF_SHA256_HMAC_OUT_SIZE}, error::{CryptoError, Result}, + util::{PbkdfSha256Hmac, PBKDF_SHA256_HMAC_OUT_SIZE}, }; /// Decrypt using AES-256 in CBC mode. /// /// Behaves similar to [decrypt_aes256_hmac], but does not validate the MAC. -pub fn decrypt_aes256(iv: &[u8; 16], data: Vec, key: GenericArray) -> Result> { +pub(crate) fn decrypt_aes256( + iv: &[u8; 16], + data: Vec, + key: &GenericArray, +) -> Result> { // Decrypt data let iv = GenericArray::from_slice(iv); let mut data = data; - let decrypted_key_slice = cbc::Decryptor::::new(&key, iv) + let decrypted_key_slice = cbc::Decryptor::::new(key, iv) .decrypt_padded_mut::(&mut data) .map_err(|_| CryptoError::KeyDecrypt)?; - // Data is decrypted in place and returns a subslice of the original Vec, to avoid cloning it, we truncate to the subslice length + // Data is decrypted in place and returns a subslice of the original Vec, to avoid cloning it, + // we truncate to the subslice length let decrypted_len = decrypted_key_slice.len(); data.truncate(decrypted_len); @@ -41,16 +43,16 @@ pub fn decrypt_aes256(iv: &[u8; 16], data: Vec, key: GenericArray) /// Decrypt using AES-256 in CBC mode with MAC. /// /// Behaves similar to [decrypt_aes256], but also validates the MAC. -pub fn decrypt_aes256_hmac( +pub(crate) fn decrypt_aes256_hmac( iv: &[u8; 16], mac: &[u8; 32], data: Vec, - mac_key: GenericArray, - key: GenericArray, + mac_key: &GenericArray, + key: &GenericArray, ) -> Result> { - let res = generate_mac(&mac_key, iv, &data)?; + let res = generate_mac(mac_key, iv, &data)?; if res.ct_ne(mac).into() { - return Err(CryptoError::InvalidMac.into()); + return Err(CryptoError::InvalidMac); } decrypt_aes256(iv, data, key) } @@ -63,7 +65,7 @@ pub fn decrypt_aes256_hmac( /// /// A AesCbc256_B64 EncString #[allow(unused)] -pub fn encrypt_aes256(data_dec: &[u8], key: GenericArray) -> ([u8; 16], Vec) { +pub(crate) fn encrypt_aes256(data_dec: &[u8], key: &GenericArray) -> ([u8; 16], Vec) { let rng = rand::thread_rng(); let (iv, data) = encrypt_aes256_internal(rng, data_dec, key); @@ -77,14 +79,14 @@ pub fn encrypt_aes256(data_dec: &[u8], key: GenericArray) -> ([u8; 16], /// ## Returns /// /// A AesCbc256_HmacSha256_B64 EncString -pub fn encrypt_aes256_hmac( +pub(crate) fn encrypt_aes256_hmac( data_dec: &[u8], - mac_key: GenericArray, - key: GenericArray, + mac_key: &GenericArray, + key: &GenericArray, ) -> Result<([u8; 16], [u8; 32], Vec)> { let rng = rand::thread_rng(); let (iv, data) = encrypt_aes256_internal(rng, data_dec, key); - let mac = generate_mac(&mac_key, &iv, &data)?; + let mac = generate_mac(mac_key, &iv, &data)?; Ok((iv, mac, data)) } @@ -97,11 +99,11 @@ pub fn encrypt_aes256_hmac( fn encrypt_aes256_internal( mut rng: impl rand::RngCore, data_dec: &[u8], - key: GenericArray, + key: &GenericArray, ) -> ([u8; 16], Vec) { let mut iv = [0u8; 16]; rng.fill_bytes(&mut iv); - let data = cbc::Encryptor::::new(&key, &iv.into()) + let data = cbc::Encryptor::::new(key, &iv.into()) .encrypt_padded_vec_mut::(data_dec); (iv, data) @@ -121,8 +123,8 @@ fn generate_mac(mac_key: &[u8], iv: &[u8], data: &[u8]) -> Result<[u8; 32]> { #[cfg(test)] mod tests { - use aes::cipher::generic_array::sequence::GenericSequence; use base64::{engine::general_purpose::STANDARD, Engine}; + use generic_array::sequence::GenericSequence; use rand::SeedableRng; use super::*; @@ -144,7 +146,7 @@ mod tests { let key = generate_generic_array(0, 1); let rng = rand_chacha::ChaCha8Rng::from_seed([0u8; 32]); - let result = encrypt_aes256_internal(rng, "EncryptMe!".as_bytes(), key); + let result = encrypt_aes256_internal(rng, "EncryptMe!".as_bytes(), &key); assert_eq!( result, ( @@ -175,7 +177,7 @@ mod tests { let key = generate_generic_array(0, 1); let data = STANDARD.decode("ByUF8vhyX4ddU9gcooznwA==").unwrap(); - let decrypted = decrypt_aes256(iv, data, key).unwrap(); + let decrypted = decrypt_aes256(iv, data, &key).unwrap(); assert_eq!(String::from_utf8(decrypted).unwrap(), "EncryptMe!"); } @@ -185,8 +187,8 @@ mod tests { let key = generate_generic_array(0, 1); let data = "EncryptMe!"; - let (iv, encrypted) = encrypt_aes256(data.as_bytes(), key); - let decrypted = decrypt_aes256(&iv, encrypted, key).unwrap(); + let (iv, encrypted) = encrypt_aes256(data.as_bytes(), &key); + let decrypted = decrypt_aes256(&iv, encrypted, &key).unwrap(); assert_eq!(String::from_utf8(decrypted).unwrap(), "EncryptMe!"); } diff --git a/crates/bitwarden-crypto/src/enc_string/asymmetric.rs b/crates/bitwarden-crypto/src/enc_string/asymmetric.rs new file mode 100644 index 000000000..04768db9e --- /dev/null +++ b/crates/bitwarden-crypto/src/enc_string/asymmetric.rs @@ -0,0 +1,291 @@ +use std::{fmt::Display, str::FromStr}; + +use base64::{engine::general_purpose::STANDARD, Engine}; +pub use internal::AsymmetricEncString; +use rsa::Oaep; +use serde::Deserialize; + +use super::{from_b64_vec, split_enc_string}; +use crate::{ + error::{CryptoError, EncStringParseError, Result}, + rsa::encrypt_rsa2048_oaep_sha1, + AsymmetricCryptoKey, AsymmetricEncryptable, KeyDecryptable, +}; + +// This module is a workaround to avoid deprecated warnings that come from the ZeroizeOnDrop +// macro expansion +#[allow(deprecated)] +mod internal { + /// # Encrypted string primitive + /// + /// [AsymmetricEncString] is a Bitwarden specific primitive that represents an asymmetrically + /// encrypted string. They are used together with the KeyDecryptable and KeyEncryptable + /// traits to encrypt and decrypt data using [crate::AsymmetricCryptoKey]s. + /// + /// The flexibility of the [AsymmetricEncString] type allows for different encryption algorithms + /// to be used which is represented by the different variants of the enum. + /// + /// ## Note + /// + /// For backwards compatibility we will rarely if ever be able to remove support for decrypting + /// old variants, but we should be opinionated in which variants are used for encrypting. + /// + /// ## Variants + /// - [Rsa2048_OaepSha256_B64](AsymmetricEncString::Rsa2048_OaepSha256_B64) + /// - [Rsa2048_OaepSha1_B64](AsymmetricEncString::Rsa2048_OaepSha1_B64) + /// + /// ## Serialization + /// + /// [AsymmetricEncString] implements [std::fmt::Display] and [std::str::FromStr] to allow for + /// easy serialization and uses a custom scheme to represent the different variants. + /// + /// The scheme is one of the following schemes: + /// - `[type].[data]` + /// + /// Where: + /// - `[type]`: is a digit number representing the variant. + /// - `[data]`: is the encrypted data. + #[derive(Clone, zeroize::ZeroizeOnDrop)] + #[allow(unused, non_camel_case_types)] + pub enum AsymmetricEncString { + /// 3 + Rsa2048_OaepSha256_B64 { data: Vec }, + /// 4 + Rsa2048_OaepSha1_B64 { data: Vec }, + /// 5 + #[deprecated] + Rsa2048_OaepSha256_HmacSha256_B64 { data: Vec, mac: Vec }, + /// 6 + #[deprecated] + Rsa2048_OaepSha1_HmacSha256_B64 { data: Vec, mac: Vec }, + } +} + +/// To avoid printing sensitive information, [AsymmetricEncString] debug prints to +/// `AsymmetricEncString`. +impl std::fmt::Debug for AsymmetricEncString { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("AsymmetricEncString").finish() + } +} + +/// Deserializes an [AsymmetricEncString] from a string. +impl FromStr for AsymmetricEncString { + type Err = CryptoError; + + fn from_str(s: &str) -> Result { + let (enc_type, parts) = split_enc_string(s); + match (enc_type, parts.len()) { + ("3", 1) => { + let data = from_b64_vec(parts[0])?; + Ok(AsymmetricEncString::Rsa2048_OaepSha256_B64 { data }) + } + ("4", 1) => { + let data = from_b64_vec(parts[0])?; + Ok(AsymmetricEncString::Rsa2048_OaepSha1_B64 { data }) + } + #[allow(deprecated)] + ("5", 2) => { + let data = from_b64_vec(parts[0])?; + let mac: Vec = from_b64_vec(parts[1])?; + Ok(AsymmetricEncString::Rsa2048_OaepSha256_HmacSha256_B64 { data, mac }) + } + #[allow(deprecated)] + ("6", 2) => { + let data = from_b64_vec(parts[0])?; + let mac: Vec = from_b64_vec(parts[1])?; + Ok(AsymmetricEncString::Rsa2048_OaepSha1_HmacSha256_B64 { data, mac }) + } + + (enc_type, parts) => Err(EncStringParseError::InvalidTypeAsymm { + enc_type: enc_type.to_string(), + parts, + } + .into()), + } + } +} + +impl Display for AsymmetricEncString { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + let parts: Vec<&[u8]> = match self { + AsymmetricEncString::Rsa2048_OaepSha256_B64 { data } => vec![data], + AsymmetricEncString::Rsa2048_OaepSha1_B64 { data } => vec![data], + #[allow(deprecated)] + AsymmetricEncString::Rsa2048_OaepSha256_HmacSha256_B64 { data, mac } => vec![data, mac], + #[allow(deprecated)] + AsymmetricEncString::Rsa2048_OaepSha1_HmacSha256_B64 { data, mac } => vec![data, mac], + }; + + let encoded_parts: Vec = parts.iter().map(|part| STANDARD.encode(part)).collect(); + + write!(f, "{}.{}", self.enc_type(), encoded_parts.join("|"))?; + + Ok(()) + } +} + +impl<'de> Deserialize<'de> for AsymmetricEncString { + fn deserialize(deserializer: D) -> Result + where + D: serde::Deserializer<'de>, + { + deserializer.deserialize_str(super::FromStrVisitor::new()) + } +} + +impl serde::Serialize for AsymmetricEncString { + fn serialize(&self, serializer: S) -> Result + where + S: serde::Serializer, + { + serializer.serialize_str(&self.to_string()) + } +} + +impl AsymmetricEncString { + /// Encrypt and produce a [AsymmetricEncString::Rsa2048_OaepSha1_B64] variant. + pub fn encrypt_rsa2048_oaep_sha1( + data_dec: &[u8], + key: &dyn AsymmetricEncryptable, + ) -> Result { + let enc = encrypt_rsa2048_oaep_sha1(key.to_public_key(), data_dec)?; + Ok(AsymmetricEncString::Rsa2048_OaepSha1_B64 { data: enc }) + } + + /// The numerical representation of the encryption type of the [AsymmetricEncString]. + const fn enc_type(&self) -> u8 { + match self { + AsymmetricEncString::Rsa2048_OaepSha256_B64 { .. } => 3, + AsymmetricEncString::Rsa2048_OaepSha1_B64 { .. } => 4, + #[allow(deprecated)] + AsymmetricEncString::Rsa2048_OaepSha256_HmacSha256_B64 { .. } => 5, + #[allow(deprecated)] + AsymmetricEncString::Rsa2048_OaepSha1_HmacSha256_B64 { .. } => 6, + } + } +} + +impl KeyDecryptable> for AsymmetricEncString { + fn decrypt_with_key(&self, key: &AsymmetricCryptoKey) -> Result> { + use AsymmetricEncString::*; + match self { + Rsa2048_OaepSha256_B64 { data } => key.key.decrypt(Oaep::new::(), data), + Rsa2048_OaepSha1_B64 { data } => key.key.decrypt(Oaep::new::(), data), + #[allow(deprecated)] + Rsa2048_OaepSha256_HmacSha256_B64 { data, .. } => { + key.key.decrypt(Oaep::new::(), data) + } + #[allow(deprecated)] + Rsa2048_OaepSha1_HmacSha256_B64 { data, .. } => { + key.key.decrypt(Oaep::new::(), data) + } + } + .map_err(|_| CryptoError::KeyDecrypt) + } +} + +impl KeyDecryptable for AsymmetricEncString { + fn decrypt_with_key(&self, key: &AsymmetricCryptoKey) -> Result { + let dec: Vec = self.decrypt_with_key(key)?; + String::from_utf8(dec).map_err(|_| CryptoError::InvalidUtf8String) + } +} + +/// Usually we wouldn't want to expose AsymmetricEncStrings in the API or the schemas. +/// But during the transition phase we will expose endpoints using the AsymmetricEncString type. +impl schemars::JsonSchema for AsymmetricEncString { + fn schema_name() -> String { + "AsymmetricEncString".to_string() + } + + fn json_schema(gen: &mut schemars::gen::SchemaGenerator) -> schemars::schema::Schema { + gen.subschema_for::() + } +} + +#[cfg(test)] +mod tests { + use super::{AsymmetricCryptoKey, AsymmetricEncString, KeyDecryptable}; + + const RSA_PRIVATE_KEY: &str = "-----BEGIN PRIVATE KEY----- +MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQCXRVrCX+2hfOQS +8HzYUS2oc/jGVTZpv+/Ryuoh9d8ihYX9dd0cYh2tl6KWdFc88lPUH11Oxqy20Rk2 +e5r/RF6T9yM0Me3NPnaKt+hlhLtfoc0h86LnhD56A9FDUfuI0dVnPcrwNv0YJIo9 +4LwxtbqBULNvXl6wJ7WAbODrCQy5ZgMVg+iH+gGpwiqsZqHt+KuoHWcN53MSPDfa +F4/YMB99U3TziJMOOJask1TEEnakMPln11PczNDazT17DXIxYrbPfutPdh6sLs6A +QOajdZijfEvepgnOe7cQ7aeatiOJFrjTApKPGxOVRzEMX4XS4xbyhH0QxQeB6l16 +l8C0uxIBAgMBAAECggEASaWfeVDA3cVzOPFSpvJm20OTE+R6uGOU+7vh36TX/POq +92qBuwbd0h0oMD32FxsXywd2IxtBDUSiFM9699qufTVuM0Q3tZw6lHDTOVG08+tP +dr8qSbMtw7PGFxN79fHLBxejjO4IrM9lapjWpxEF+11x7r+wM+0xRZQ8sNFYG46a +PfIaty4BGbL0I2DQ2y8I57iBCAy69eht59NLMm27fRWGJIWCuBIjlpfzET1j2HLX +UIh5bTBNzqaN039WH49HczGE3mQKVEJZc/efk3HaVd0a1Sjzyn0QY+N1jtZN3jTR +buDWA1AknkX1LX/0tUhuS3/7C3ejHxjw4Dk1ZLo5/QKBgQDIWvqFn0+IKRSu6Ua2 +hDsufIHHUNLelbfLUMmFthxabcUn4zlvIscJO00Tq/ezopSRRvbGiqnxjv/mYxuc +vOUBeZtlus0Q9RTACBtw9TGoNTmQbEunJ2FOSlqbQxkBBAjgGEppRPt30iGj/VjA +hCATq2MYOa/X4dVR51BqQAFIEwKBgQDBSIfTFKC/hDk6FKZlgwvupWYJyU9Rkyfs +tPErZFmzoKhPkQ3YORo2oeAYmVUbS9I2iIYpYpYQJHX8jMuCbCz4ONxTCuSIXYQY +UcUq4PglCKp31xBAE6TN8SvhfME9/MvuDssnQinAHuF0GDAhF646T3LLS1not6Vs +zv7brwSoGwKBgQC88v/8cGfi80ssQZeMnVvq1UTXIeQcQnoY5lGHJl3K8mbS3TnX +E6c9j417Fdz+rj8KWzBzwWXQB5pSPflWcdZO886Xu/mVGmy9RWgLuVFhXwCwsVEP +jNX5ramRb0/vY0yzenUCninBsIxFSbIfrPtLUYCc4hpxr+sr2Mg/y6jpvQKBgBez +MRRs3xkcuXepuI2R+BCXL1/b02IJTUf1F+1eLLGd7YV0H+J3fgNc7gGWK51hOrF9 +JBZHBGeOUPlaukmPwiPdtQZpu4QNE3l37VlIpKTF30E6mb+BqR+nht3rUjarnMXg +AoEZ18y6/KIjpSMpqC92Nnk/EBM9EYe6Cf4eA9ApAoGAeqEUg46UTlJySkBKURGp +Is3v1kkf5I0X8DnOhwb+HPxNaiEdmO7ckm8+tPVgppLcG0+tMdLjigFQiDUQk2y3 +WjyxP5ZvXu7U96jaJRI8PFMoE06WeVYcdIzrID2HvqH+w0UQJFrLJ/0Mn4stFAEz +XKZBokBGnjFnTnKcs7nv/O8= +-----END PRIVATE KEY-----"; + + #[test] + fn test_enc_string_rsa2048_oaep_sha256_b64() { + let private_key = AsymmetricCryptoKey::from_pem(RSA_PRIVATE_KEY).unwrap(); + let enc_str: &str = "3.YFqzW9LL/uLjCnl0RRLtndzGJ1FV27mcwQwGjfJPOVrgCX9nJSUYCCDd0iTIyOZ/zRxG47b6L1Z3qgkEfcxjmrSBq60gijc3E2TBMAg7OCLVcjORZ+i1sOVOudmOPWro6uA8refMrg4lqbieDlbLMzjVEwxfi5WpcL876cD0vYyRwvLO3bzFrsE7x33HHHtZeOPW79RqMn5efsB5Dj9wVheC9Ix9AYDjbo+rjg9qR6guwKmS7k2MSaIQlrDR7yu8LP+ePtiSjx+gszJV5jQGfcx60dtiLQzLS/mUD+RmU7B950Bpx0H7x56lT5yXZbWK5YkoP6qd8B8D2aKbP68Ywg=="; + let enc_string: AsymmetricEncString = enc_str.parse().unwrap(); + + assert_eq!(enc_string.enc_type(), 3); + + let res: String = enc_string.decrypt_with_key(&private_key).unwrap(); + assert_eq!(res, "EncryptMe!"); + } + + #[test] + fn test_enc_string_rsa2048_oaep_sha1_b64() { + let private_key = AsymmetricCryptoKey::from_pem(RSA_PRIVATE_KEY).unwrap(); + let enc_str: &str = "4.ZheRb3PCfAunyFdQYPfyrFqpuvmln9H9w5nDjt88i5A7ug1XE0LJdQHCIYJl0YOZ1gCOGkhFu/CRY2StiLmT3iRKrrVBbC1+qRMjNNyDvRcFi91LWsmRXhONVSPjywzrJJXglsztDqGkLO93dKXNhuKpcmtBLsvgkphk/aFvxbaOvJ/FHdK/iV0dMGNhc/9tbys8laTdwBlI5xIChpRcrfH+XpSFM88+Bu03uK67N9G6eU1UmET+pISJwJvMuIDMqH+qkT7OOzgL3t6I0H2LDj+CnsumnQmDsvQzDiNfTR0IgjpoE9YH2LvPXVP2wVUkiTwXD9cG/E7XeoiduHyHjw=="; + let enc_string: AsymmetricEncString = enc_str.parse().unwrap(); + + assert_eq!(enc_string.enc_type(), 4); + + let res: String = enc_string.decrypt_with_key(&private_key).unwrap(); + assert_eq!(res, "EncryptMe!"); + } + + #[test] + fn test_enc_string_rsa2048_oaep_sha1_hmac_sha256_b64() { + let private_key = AsymmetricCryptoKey::from_pem(RSA_PRIVATE_KEY).unwrap(); + let enc_str: &str = "6.ThnNc67nNr7GELyuhGGfsXNP2zJnNqhrIsjntEQ27r2qmn8vwdHbTbfO0cwt6YgSibDN0PjiCZ1O3Wb/IFq+vwvyRwFqF9145wBF8CQCbkhV+M0XvO99kh0daovtt120Nve/5ETI5PbPag9VdalKRQWZypJaqQHm5TAQVf4F5wtLlCLMBkzqTk+wkFe7BPMTGn07T+O3eJbTxXvyMZewQ7icJF0MZVA7VyWX9qElmZ89FCKowbf1BMr5pbcQ+0KdXcSVW3to43VkTp7k7COwsuH3M/i1AuVP5YN8ixjyRpvaeGqX/ap2nCHK2Wj5VxgCGT7XEls6ZknnAp9nB9qVjQ==|s3ntw5H/KKD/qsS0lUghTHl5Sm9j6m7YEdNHf0OeAFQ="; + let enc_string: AsymmetricEncString = enc_str.parse().unwrap(); + + assert_eq!(enc_string.enc_type(), 6); + + let res: String = enc_string.decrypt_with_key(&private_key).unwrap(); + assert_eq!(res, "EncryptMe!"); + } + + #[test] + fn test_enc_string_serialization() { + #[derive(serde::Serialize, serde::Deserialize)] + struct Test { + key: AsymmetricEncString, + } + + let cipher = "6.ThnNc67nNr7GELyuhGGfsXNP2zJnNqhrIsjntEQ27r2qmn8vwdHbTbfO0cwt6YgSibDN0PjiCZ1O3Wb/IFq+vwvyRwFqF9145wBF8CQCbkhV+M0XvO99kh0daovtt120Nve/5ETI5PbPag9VdalKRQWZypJaqQHm5TAQVf4F5wtLlCLMBkzqTk+wkFe7BPMTGn07T+O3eJbTxXvyMZewQ7icJF0MZVA7VyWX9qElmZ89FCKowbf1BMr5pbcQ+0KdXcSVW3to43VkTp7k7COwsuH3M/i1AuVP5YN8ixjyRpvaeGqX/ap2nCHK2Wj5VxgCGT7XEls6ZknnAp9nB9qVjQ==|s3ntw5H/KKD/qsS0lUghTHl5Sm9j6m7YEdNHf0OeAFQ="; + let serialized = format!("{{\"key\":\"{cipher}\"}}"); + + let t = serde_json::from_str::(&serialized).unwrap(); + assert_eq!(t.key.enc_type(), 6); + assert_eq!(t.key.to_string(), cipher); + assert_eq!(serde_json::to_string(&t).unwrap(), serialized); + } +} diff --git a/crates/bitwarden/src/crypto/enc_string/mod.rs b/crates/bitwarden-crypto/src/enc_string/mod.rs similarity index 96% rename from crates/bitwarden/src/crypto/enc_string/mod.rs rename to crates/bitwarden-crypto/src/enc_string/mod.rs index a998e6056..e1433821f 100644 --- a/crates/bitwarden/src/crypto/enc_string/mod.rs +++ b/crates/bitwarden-crypto/src/enc_string/mod.rs @@ -1,15 +1,15 @@ +/// Encrypted string types mod asymmetric; mod symmetric; use std::str::FromStr; -pub use asymmetric::AsymmEncString; +pub use asymmetric::AsymmetricEncString; use base64::{engine::general_purpose::STANDARD, Engine}; pub use symmetric::EncString; use crate::error::{EncStringParseError, Result}; -#[cfg(feature = "mobile")] fn check_length(buf: &[u8], expected: usize) -> Result<()> { if buf.len() < expected { return Err(EncStringParseError::InvalidLength { diff --git a/crates/bitwarden/src/crypto/enc_string/symmetric.rs b/crates/bitwarden-crypto/src/enc_string/symmetric.rs similarity index 84% rename from crates/bitwarden/src/crypto/enc_string/symmetric.rs rename to crates/bitwarden-crypto/src/enc_string/symmetric.rs index 798994a1c..a76315e96 100644 --- a/crates/bitwarden/src/crypto/enc_string/symmetric.rs +++ b/crates/bitwarden-crypto/src/enc_string/symmetric.rs @@ -1,22 +1,21 @@ use std::{fmt::Display, str::FromStr}; -use aes::cipher::{generic_array::GenericArray, typenum::U32}; +use aes::cipher::typenum::U32; use base64::{engine::general_purpose::STANDARD, Engine}; +use generic_array::GenericArray; use serde::Deserialize; -#[cfg(feature = "mobile")] -use super::check_length; -use super::{from_b64, from_b64_vec, split_enc_string}; +use super::{check_length, from_b64, from_b64_vec, split_enc_string}; use crate::{ - crypto::{decrypt_aes256_hmac, KeyDecryptable, KeyEncryptable, LocateKey, SymmetricCryptoKey}, - error::{CryptoError, EncStringParseError, Error, Result}, + error::{CryptoError, EncStringParseError, Result}, + KeyDecryptable, KeyEncryptable, LocateKey, SymmetricCryptoKey, }; /// # Encrypted string primitive /// -/// [EncString] is a Bitwarden specific primitive that represents a symmetrically encrypted string. They are -/// are used together with the [KeyDecryptable] and [KeyEncryptable] traits to encrypt and decrypt -/// data using [SymmetricCryptoKey]s. +/// [EncString] is a Bitwarden specific primitive that represents a symmetrically encrypted string. +/// They are are used together with the [KeyDecryptable] and [KeyEncryptable] traits to encrypt and +/// decrypt data using [SymmetricCryptoKey]s. /// /// The flexibility of the [EncString] type allows for different encryption algorithms to be used /// which is represented by the different variants of the enum. @@ -45,7 +44,7 @@ use crate::{ /// - `[iv]`: (optional) is the initialization vector used for encryption. /// - `[data]`: is the encrypted data. /// - `[mac]`: (optional) is the MAC used to validate the integrity of the data. -#[derive(Clone)] +#[derive(Clone, zeroize::ZeroizeOnDrop)] #[allow(unused, non_camel_case_types)] pub enum EncString { /// 0 @@ -73,7 +72,7 @@ impl std::fmt::Debug for EncString { /// Deserializes an [EncString] from a string. impl FromStr for EncString { - type Err = Error; + type Err = CryptoError; fn from_str(s: &str) -> Result { let (enc_type, parts) = split_enc_string(s); @@ -107,13 +106,11 @@ impl FromStr for EncString { impl EncString { /// Synthetic sugar for mapping `Option` to `Result>` - #[cfg(feature = "internal")] - pub(crate) fn try_from_optional(s: Option) -> Result, Error> { + pub fn try_from_optional(s: Option) -> Result, CryptoError> { s.map(|s| s.parse()).transpose() } - #[cfg(feature = "mobile")] - pub(crate) fn from_buffer(buf: &[u8]) -> Result { + pub fn from_buffer(buf: &[u8]) -> Result { if buf.is_empty() { return Err(EncStringParseError::NoType.into()); } @@ -147,8 +144,7 @@ impl EncString { } } - #[cfg(feature = "mobile")] - pub(crate) fn to_buffer(&self) -> Result> { + pub fn to_buffer(&self) -> Result> { let mut buf; match self { @@ -207,12 +203,12 @@ impl serde::Serialize for EncString { } impl EncString { - pub(crate) fn encrypt_aes256_hmac( + pub fn encrypt_aes256_hmac( data_dec: &[u8], - mac_key: GenericArray, - key: GenericArray, + mac_key: &GenericArray, + key: &GenericArray, ) -> Result { - let (iv, mac, data) = crate::crypto::encrypt_aes256_hmac(data_dec, mac_key, key)?; + let (iv, mac, data) = crate::aes::encrypt_aes256_hmac(data_dec, mac_key, key)?; Ok(EncString::AesCbc256_HmacSha256_B64 { iv, mac, data }) } @@ -227,35 +223,40 @@ impl EncString { } impl LocateKey for EncString {} -impl KeyEncryptable for &[u8] { +impl KeyEncryptable for &[u8] { fn encrypt_with_key(self, key: &SymmetricCryptoKey) -> Result { - EncString::encrypt_aes256_hmac(self, key.mac_key.ok_or(CryptoError::InvalidMac)?, key.key) + EncString::encrypt_aes256_hmac( + self, + key.mac_key.as_ref().ok_or(CryptoError::InvalidMac)?, + &key.key, + ) } } -impl KeyDecryptable> for EncString { +impl KeyDecryptable> for EncString { fn decrypt_with_key(&self, key: &SymmetricCryptoKey) -> Result> { match self { EncString::AesCbc256_HmacSha256_B64 { iv, mac, data } => { - let mac_key = key.mac_key.ok_or(CryptoError::InvalidMac)?; - let dec = decrypt_aes256_hmac(iv, mac, data.clone(), mac_key, key.key)?; + let mac_key = key.mac_key.as_ref().ok_or(CryptoError::InvalidMac)?; + let dec = + crate::aes::decrypt_aes256_hmac(iv, mac, data.clone(), mac_key, &key.key)?; Ok(dec) } - _ => Err(CryptoError::InvalidKey.into()), + _ => Err(CryptoError::InvalidKey), } } } -impl KeyEncryptable for String { +impl KeyEncryptable for String { fn encrypt_with_key(self, key: &SymmetricCryptoKey) -> Result { self.as_bytes().encrypt_with_key(key) } } -impl KeyDecryptable for EncString { +impl KeyDecryptable for EncString { fn decrypt_with_key(&self, key: &SymmetricCryptoKey) -> Result { let dec: Vec = self.decrypt_with_key(key)?; - String::from_utf8(dec).map_err(|_| CryptoError::InvalidUtf8String.into()) + String::from_utf8(dec).map_err(|_| CryptoError::InvalidUtf8String) } } @@ -274,11 +275,11 @@ impl schemars::JsonSchema for EncString { #[cfg(test)] mod tests { use super::EncString; - use crate::crypto::{KeyDecryptable, KeyEncryptable, SymmetricCryptoKey}; + use crate::{derive_symmetric_key, KeyDecryptable, KeyEncryptable}; #[test] fn test_enc_string_roundtrip() { - let key = SymmetricCryptoKey::generate("test"); + let key = derive_symmetric_key("test"); let test_string = "encrypted_test_string".to_string(); let cipher = test_string.clone().encrypt_with_key(&key).unwrap(); @@ -303,7 +304,6 @@ mod tests { assert_eq!(serde_json::to_string(&t).unwrap(), serialized); } - #[cfg(feature = "mobile")] #[test] fn test_enc_from_to_buffer() { let enc_str: &str = "2.pMS6/icTQABtulw52pq2lg==|XXbxKxDTh+mWiN1HjH2N1w==|Q6PkuT+KX/axrgN9ubD5Ajk2YNwxQkgs3WJM0S0wtG8="; diff --git a/crates/bitwarden/src/crypto/encryptable.rs b/crates/bitwarden-crypto/src/encryptable.rs similarity index 51% rename from crates/bitwarden/src/crypto/encryptable.rs rename to crates/bitwarden-crypto/src/encryptable.rs index f1bc78f15..9f1256afa 100644 --- a/crates/bitwarden/src/crypto/encryptable.rs +++ b/crates/bitwarden-crypto/src/encryptable.rs @@ -2,17 +2,16 @@ use std::{collections::HashMap, hash::Hash}; use uuid::Uuid; -use crate::{ - client::encryption_settings::EncryptionSettings, - error::{Error, Result}, -}; +use crate::{CryptoError, KeyDecryptable, KeyEncryptable, Result, SymmetricCryptoKey}; -use super::{KeyDecryptable, KeyEncryptable, SymmetricCryptoKey}; +pub trait KeyContainer { + fn get_key(&self, org_id: &Option) -> Option<&SymmetricCryptoKey>; +} pub trait LocateKey { fn locate_key<'a>( &self, - enc: &'a EncryptionSettings, + enc: &'a dyn KeyContainer, org_id: &Option, ) -> Option<&'a SymmetricCryptoKey> { enc.get_key(org_id) @@ -21,36 +20,40 @@ pub trait LocateKey { /// Deprecated: please use LocateKey and KeyDecryptable instead pub trait Encryptable { - fn encrypt(self, enc: &EncryptionSettings, org_id: &Option) -> Result; + fn encrypt(self, enc: &dyn KeyContainer, org_id: &Option) -> Result; } /// Deprecated: please use LocateKey and KeyDecryptable instead pub trait Decryptable { - fn decrypt(&self, enc: &EncryptionSettings, org_id: &Option) -> Result; + fn decrypt(&self, enc: &dyn KeyContainer, org_id: &Option) -> Result; } -impl + LocateKey, Output> Encryptable for T { - fn encrypt(self, enc: &EncryptionSettings, org_id: &Option) -> Result { - let key = self.locate_key(enc, org_id).ok_or(Error::VaultLocked)?; +impl + LocateKey, Output> Encryptable for T { + fn encrypt(self, enc: &dyn KeyContainer, org_id: &Option) -> Result { + let key = self + .locate_key(enc, org_id) + .ok_or(CryptoError::MissingKey)?; self.encrypt_with_key(key) } } -impl + LocateKey, Output> Decryptable for T { - fn decrypt(&self, enc: &EncryptionSettings, org_id: &Option) -> Result { - let key = self.locate_key(enc, org_id).ok_or(Error::VaultLocked)?; +impl + LocateKey, Output> Decryptable for T { + fn decrypt(&self, enc: &dyn KeyContainer, org_id: &Option) -> Result { + let key = self + .locate_key(enc, org_id) + .ok_or(CryptoError::MissingKey)?; self.decrypt_with_key(key) } } impl, Output> Encryptable> for Vec { - fn encrypt(self, enc: &EncryptionSettings, org_id: &Option) -> Result> { + fn encrypt(self, enc: &dyn KeyContainer, org_id: &Option) -> Result> { self.into_iter().map(|e| e.encrypt(enc, org_id)).collect() } } impl, Output> Decryptable> for Vec { - fn decrypt(&self, enc: &EncryptionSettings, org_id: &Option) -> Result> { + fn decrypt(&self, enc: &dyn KeyContainer, org_id: &Option) -> Result> { self.iter().map(|e| e.decrypt(enc, org_id)).collect() } } @@ -58,11 +61,7 @@ impl, Output> Decryptable> for Vec { impl, Output, Id: Hash + Eq> Encryptable> for HashMap { - fn encrypt( - self, - enc: &EncryptionSettings, - org_id: &Option, - ) -> Result> { + fn encrypt(self, enc: &dyn KeyContainer, org_id: &Option) -> Result> { self.into_iter() .map(|(id, e)| Ok((id, e.encrypt(enc, org_id)?))) .collect() @@ -74,7 +73,7 @@ impl, Output, Id: Hash + Eq + Copy> Decryptable, ) -> Result> { self.iter() diff --git a/crates/bitwarden-crypto/src/error.rs b/crates/bitwarden-crypto/src/error.rs new file mode 100644 index 000000000..cf9a9b048 --- /dev/null +++ b/crates/bitwarden-crypto/src/error.rs @@ -0,0 +1,60 @@ +use std::fmt::Debug; + +use thiserror::Error; + +use crate::fingerprint::FingerprintError; + +#[derive(Debug, Error)] +pub enum CryptoError { + #[error("The provided key is not the expected type")] + InvalidKey, + #[error("The cipher's MAC doesn't match the expected value")] + InvalidMac, + #[error("Error while decrypting EncString")] + KeyDecrypt, + #[error("The cipher key has an invalid length")] + InvalidKeyLen, + #[error("The value is not a valid UTF8 String")] + InvalidUtf8String, + #[error("Missing Key")] + MissingKey, + + #[error("EncString error, {0}")] + EncString(#[from] EncStringParseError), + + #[error("Rsa error, {0}")] + RsaError(#[from] RsaError), + + #[error("Fingerprint error, {0}")] + FingerprintError(#[from] FingerprintError), + + #[error("Number is zero")] + ZeroNumber, +} + +#[derive(Debug, Error)] +pub enum EncStringParseError { + #[error("No type detected, missing '.' separator")] + NoType, + #[error("Invalid symmetric type, got type {enc_type} with {parts} parts")] + InvalidTypeSymm { enc_type: String, parts: usize }, + #[error("Invalid asymmetric type, got type {enc_type} with {parts} parts")] + InvalidTypeAsymm { enc_type: String, parts: usize }, + #[error("Error decoding base64: {0}")] + InvalidBase64(#[from] base64::DecodeError), + #[error("Invalid length: expected {expected}, got {got}")] + InvalidLength { expected: usize, got: usize }, +} + +#[derive(Debug, Error)] +pub enum RsaError { + #[error("Unable to create public key")] + CreatePublicKey, + #[error("Unable to create private key")] + CreatePrivateKey, + #[error("Rsa error, {0}")] + Rsa(#[from] rsa::Error), +} + +/// Alias for `Result`. +pub(crate) type Result = std::result::Result; diff --git a/crates/bitwarden/src/crypto/fingerprint.rs b/crates/bitwarden-crypto/src/fingerprint.rs similarity index 81% rename from crates/bitwarden/src/crypto/fingerprint.rs rename to crates/bitwarden-crypto/src/fingerprint.rs index 707a07ca6..d3e69d577 100644 --- a/crates/bitwarden/src/crypto/fingerprint.rs +++ b/crates/bitwarden-crypto/src/fingerprint.rs @@ -1,27 +1,34 @@ +//! # Fingerprint +//! +//! Provides a way to derive fingerprints from fingerprint material and public keys. This is most +//! commonly used for account fingerprints, where the fingerprint material is the user's id and the +//! public key is the user's public key. + use num_bigint::BigUint; use num_traits::cast::ToPrimitive; use sha2::Digest; +use thiserror::Error; -use crate::{error::Result, wordlist::EFF_LONG_WORD_LIST}; +use crate::{error::Result, wordlist::EFF_LONG_WORD_LIST, CryptoError}; /// Computes a fingerprint of the given `fingerprint_material` using the given `public_key`. /// /// This is commonly used for account fingerprints. With the following arguments: /// - `fingerprint_material`: user's id. /// - `public_key`: user's public key. -pub(crate) fn fingerprint(fingerprint_material: &str, public_key: &[u8]) -> Result { +pub fn fingerprint(fingerprint_material: &str, public_key: &[u8]) -> Result { let mut h = sha2::Sha256::new(); h.update(public_key); h.finalize(); let hkdf = - hkdf::Hkdf::::from_prk(public_key).map_err(|_| "hkdf::InvalidLength")?; + hkdf::Hkdf::::from_prk(public_key).map_err(|_| CryptoError::InvalidKeyLen)?; let mut user_fingerprint = [0u8; 32]; hkdf.expand(fingerprint_material.as_bytes(), &mut user_fingerprint) - .map_err(|_| "hkdf::expand")?; + .map_err(|_| CryptoError::InvalidKeyLen)?; - Ok(hash_word(user_fingerprint).unwrap()) + hash_word(user_fingerprint) } /// Derive a 5 word phrase from a 32 byte hash. @@ -34,7 +41,7 @@ fn hash_word(hash: [u8; 32]) -> Result { let hash_arr: Vec = hash.to_vec(); let entropy_available = hash_arr.len() * 4; if num_words as f64 * entropy_per_word > entropy_available as f64 { - return Err("Output entropy of hash function is too small".into()); + return Err(FingerprintError::EntropyTooSmall.into()); } let mut phrase = Vec::new(); @@ -50,6 +57,12 @@ fn hash_word(hash: [u8; 32]) -> Result { Ok(phrase.join("-")) } +#[derive(Debug, Error)] +pub enum FingerprintError { + #[error("Entropy is too small")] + EntropyTooSmall, +} + #[cfg(test)] mod tests { use super::fingerprint; diff --git a/crates/bitwarden-crypto/src/keys/asymmetric_crypto_key.rs b/crates/bitwarden-crypto/src/keys/asymmetric_crypto_key.rs new file mode 100644 index 000000000..be523bbc6 --- /dev/null +++ b/crates/bitwarden-crypto/src/keys/asymmetric_crypto_key.rs @@ -0,0 +1,225 @@ +use std::pin::Pin; + +use rsa::{pkcs8::DecodePublicKey, RsaPrivateKey, RsaPublicKey}; + +use super::key_encryptable::CryptoKey; +use crate::error::{CryptoError, Result}; + +/// Trait to allow both [`AsymmetricCryptoKey`] and [`AsymmetricPublicCryptoKey`] to be used to +/// encrypt [AsymmetricEncString](crate::AsymmetricEncString). +pub trait AsymmetricEncryptable { + fn to_public_key(&self) -> &RsaPublicKey; +} + +/// An asymmetric public encryption key. Can only encrypt +/// [AsymmetricEncString](crate::AsymmetricEncString), usually accompanied by a +/// [AsymmetricCryptoKey] +pub struct AsymmetricPublicCryptoKey { + pub(crate) key: RsaPublicKey, +} + +impl AsymmetricPublicCryptoKey { + /// Build a public key from the SubjectPublicKeyInfo DER. + pub fn from_der(der: &[u8]) -> Result { + Ok(Self { + key: rsa::RsaPublicKey::from_public_key_der(der) + .map_err(|_| CryptoError::InvalidKey)?, + }) + } +} + +impl AsymmetricEncryptable for AsymmetricPublicCryptoKey { + fn to_public_key(&self) -> &RsaPublicKey { + &self.key + } +} + +/// An asymmetric encryption key. Contains both the public and private key. Can be used to both +/// encrypt and decrypt [`AsymmetricEncString`](crate::AsymmetricEncString). +pub struct AsymmetricCryptoKey { + // RsaPrivateKey is not a Copy type so this isn't completely necessary, but + // to keep the compiler from making stack copies when moving this struct around, + // we use a Box to keep the values on the heap. We also pin the box to make sure + // that the contents can't be pulled out of the box and moved + pub(crate) key: Pin>, +} + +// Note that RsaPrivateKey already implements ZeroizeOnDrop, so we don't need to do anything +// We add this assertion to make sure that this is still true in the future +const _: () = { + fn assert_zeroize_on_drop() {} + fn assert_all() { + assert_zeroize_on_drop::(); + } +}; + +impl zeroize::ZeroizeOnDrop for AsymmetricCryptoKey {} + +impl AsymmetricCryptoKey { + /// Generate a random AsymmetricCryptoKey (RSA-2048). + pub fn generate(rng: &mut R) -> Self { + let bits = 2048; + + Self { + key: Box::pin(RsaPrivateKey::new(rng, bits).expect("failed to generate a key")), + } + } + + pub fn from_pem(pem: &str) -> Result { + use rsa::pkcs8::DecodePrivateKey; + Ok(Self { + key: Box::pin(RsaPrivateKey::from_pkcs8_pem(pem).map_err(|_| CryptoError::InvalidKey)?), + }) + } + + pub fn from_der(der: &[u8]) -> Result { + use rsa::pkcs8::DecodePrivateKey; + Ok(Self { + key: Box::pin(RsaPrivateKey::from_pkcs8_der(der).map_err(|_| CryptoError::InvalidKey)?), + }) + } + + pub fn to_der(&self) -> Result> { + use rsa::pkcs8::EncodePrivateKey; + Ok(self + .key + .to_pkcs8_der() + .map_err(|_| CryptoError::InvalidKey)? + .as_bytes() + .to_owned()) + } + + pub fn to_public_der(&self) -> Result> { + use rsa::pkcs8::EncodePublicKey; + Ok(self + .to_public_key() + .to_public_key_der() + .map_err(|_| CryptoError::InvalidKey)? + .as_bytes() + .to_owned()) + } +} + +impl AsymmetricEncryptable for AsymmetricCryptoKey { + fn to_public_key(&self) -> &RsaPublicKey { + (*self.key).as_ref() + } +} + +impl CryptoKey for AsymmetricCryptoKey {} + +// We manually implement these to make sure we don't print any sensitive data +impl std::fmt::Debug for AsymmetricCryptoKey { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("AsymmetricCryptoKey").finish() + } +} + +#[cfg(test)] +mod tests { + use base64::{engine::general_purpose::STANDARD, Engine}; + + use crate::{ + AsymmetricCryptoKey, AsymmetricEncString, AsymmetricPublicCryptoKey, KeyDecryptable, + }; + + #[test] + fn test_asymmetric_crypto_key() { + let pem_key_str = "-----BEGIN PRIVATE KEY----- +MIIEwAIBADANBgkqhkiG9w0BAQEFAASCBKowggSmAgEAAoIBAQDiTQVuzhdygFz5 +qv14i+XFDGTnDravzUQT1hPKPGUZOUSZ1gwdNgkWqOIaOnR65BHEnL0sp4bnuiYc +afeK2JAW5Sc8Z7IxBNSuAwhQmuKx3RochMIiuCkI2/p+JvUQoJu6FBNm8OoJ4Cwm +qqHGZESMfnpQDCuDrB3JdJEdXhtmnl0C48sGjOk3WaBMcgGqn8LbJDUlyu1zdqyv +b0waJf0iV4PJm2fkUl7+57D/2TkpbCqURVnZK1FFIEg8mr6FzSN1F2pOfktkNYZw +P7MSNR7o81CkRSCMr7EkIVa+MZYMBx106BMK7FXgWB7nbSpsWKxBk7ZDHkID2fam +rEcVtrzDAgMBAAECggEBAKwq9OssGGKgjhvUnyrLJHAZ0dqIMyzk+dotkLjX4gKi +szJmyqiep6N5sStLNbsZMPtoU/RZMCW0VbJgXFhiEp2YkZU/Py5UAoqw++53J+kx +0d/IkPphKbb3xUec0+1mg5O6GljDCQuiZXS1dIa/WfeZcezclW6Dz9WovY6ePjJ+ +8vEBR1icbNKzyeINd6MtPtpcgQPHtDwHvhPyUDbKDYGbLvjh9nui8h4+ZUlXKuVR +jB0ChxiKV1xJRjkrEVoulOOicd5r597WfB2ghax3pvRZ4MdXemCXm3gQYqPVKach +vGU+1cPQR/MBJZpxT+EZA97xwtFS3gqwbxJaNFcoE8ECgYEA9OaeYZhQPDo485tI +1u/Z7L/3PNape9hBQIXoW7+MgcQ5NiWqYh8Jnj43EIYa0wM/ECQINr1Za8Q5e6KR +J30FcU+kfyjuQ0jeXdNELGU/fx5XXNg/vV8GevHwxRlwzqZTCg6UExUZzbYEQqd7 +l+wPyETGeua5xCEywA1nX/D101kCgYEA7I6aMFjhEjO71RmzNhqjKJt6DOghoOfQ +TjhaaanNEhLYSbenFz1mlb21mW67ulmz162saKdIYLxQNJIP8ZPmxh4ummOJI8w9 +ClHfo8WuCI2hCjJ19xbQJocSbTA5aJg6lA1IDVZMDbQwsnAByPRGpaLHBT/Q9Bye +KvCMB+9amXsCgYEAx65yXSkP4sumPBrVHUub6MntERIGRxBgw/drKcPZEMWp0FiN +wEuGUBxyUWrG3F69QK/gcqGZE6F/LSu0JvptQaKqgXQiMYJsrRvhbkFvsHpQyUcZ +UZL1ebFjm5HOxPAgrQaN/bEqxOwwNRjSUWEMzUImg3c06JIZCzbinvudtKECgYEA +kY3JF/iIPI/yglP27lKDlCfeeHSYxI3+oTKRhzSAxx8rUGidenJAXeDGDauR/T7W +pt3pGNfddBBK9Z3uC4Iq3DqUCFE4f/taj7ADAJ1Q0Vh7/28/IJM77ojr8J1cpZwN +Zy2o6PPxhfkagaDjqEeN9Lrs5LD4nEvDkr5CG1vOjmMCgYEAvIBFKRm31NyF8jLi +CVuPwC5PzrW5iThDmsWTaXFpB3esUsbICO2pEz872oeQS+Em4GO5vXUlpbbFPzup +PFhA8iMJ8TAvemhvc7oM0OZqpU6p3K4seHf6BkwLxumoA3vDJfovu9RuXVcJVOnf +DnqOsltgPomWZ7xVfMkm9niL2OA= +-----END PRIVATE KEY-----"; + + let der_key_vec = STANDARD.decode("MIIEwAIBADANBgkqhkiG9w0BAQEFAASCBKowggSmAgEAAoIBAQDiTQVuzhdygFz5qv14i+XFDGTnDravzUQT1hPKPGUZOUSZ1gwdNgkWqOIaOnR65BHEnL0sp4bnuiYcafeK2JAW5Sc8Z7IxBNSuAwhQmuKx3RochMIiuCkI2/p+JvUQoJu6FBNm8OoJ4CwmqqHGZESMfnpQDCuDrB3JdJEdXhtmnl0C48sGjOk3WaBMcgGqn8LbJDUlyu1zdqyvb0waJf0iV4PJm2fkUl7+57D/2TkpbCqURVnZK1FFIEg8mr6FzSN1F2pOfktkNYZwP7MSNR7o81CkRSCMr7EkIVa+MZYMBx106BMK7FXgWB7nbSpsWKxBk7ZDHkID2famrEcVtrzDAgMBAAECggEBAKwq9OssGGKgjhvUnyrLJHAZ0dqIMyzk+dotkLjX4gKiszJmyqiep6N5sStLNbsZMPtoU/RZMCW0VbJgXFhiEp2YkZU/Py5UAoqw++53J+kx0d/IkPphKbb3xUec0+1mg5O6GljDCQuiZXS1dIa/WfeZcezclW6Dz9WovY6ePjJ+8vEBR1icbNKzyeINd6MtPtpcgQPHtDwHvhPyUDbKDYGbLvjh9nui8h4+ZUlXKuVRjB0ChxiKV1xJRjkrEVoulOOicd5r597WfB2ghax3pvRZ4MdXemCXm3gQYqPVKachvGU+1cPQR/MBJZpxT+EZA97xwtFS3gqwbxJaNFcoE8ECgYEA9OaeYZhQPDo485tI1u/Z7L/3PNape9hBQIXoW7+MgcQ5NiWqYh8Jnj43EIYa0wM/ECQINr1Za8Q5e6KRJ30FcU+kfyjuQ0jeXdNELGU/fx5XXNg/vV8GevHwxRlwzqZTCg6UExUZzbYEQqd7l+wPyETGeua5xCEywA1nX/D101kCgYEA7I6aMFjhEjO71RmzNhqjKJt6DOghoOfQTjhaaanNEhLYSbenFz1mlb21mW67ulmz162saKdIYLxQNJIP8ZPmxh4ummOJI8w9ClHfo8WuCI2hCjJ19xbQJocSbTA5aJg6lA1IDVZMDbQwsnAByPRGpaLHBT/Q9ByeKvCMB+9amXsCgYEAx65yXSkP4sumPBrVHUub6MntERIGRxBgw/drKcPZEMWp0FiNwEuGUBxyUWrG3F69QK/gcqGZE6F/LSu0JvptQaKqgXQiMYJsrRvhbkFvsHpQyUcZUZL1ebFjm5HOxPAgrQaN/bEqxOwwNRjSUWEMzUImg3c06JIZCzbinvudtKECgYEAkY3JF/iIPI/yglP27lKDlCfeeHSYxI3+oTKRhzSAxx8rUGidenJAXeDGDauR/T7Wpt3pGNfddBBK9Z3uC4Iq3DqUCFE4f/taj7ADAJ1Q0Vh7/28/IJM77ojr8J1cpZwNZy2o6PPxhfkagaDjqEeN9Lrs5LD4nEvDkr5CG1vOjmMCgYEAvIBFKRm31NyF8jLiCVuPwC5PzrW5iThDmsWTaXFpB3esUsbICO2pEz872oeQS+Em4GO5vXUlpbbFPzupPFhA8iMJ8TAvemhvc7oM0OZqpU6p3K4seHf6BkwLxumoA3vDJfovu9RuXVcJVOnfDnqOsltgPomWZ7xVfMkm9niL2OA=").unwrap(); + + // Load the two different formats and check they are the same key + let pem_key = AsymmetricCryptoKey::from_pem(pem_key_str).unwrap(); + let der_key = AsymmetricCryptoKey::from_der(&der_key_vec).unwrap(); + assert_eq!(pem_key.key, der_key.key); + + // Check that the keys can be converted back to DER + assert_eq!(der_key.to_der().unwrap(), der_key_vec); + assert_eq!(pem_key.to_der().unwrap(), der_key_vec); + } + + #[test] + fn test_encrypt_public_decrypt_private() { + let private_key = STANDARD + .decode(concat!( + "MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQCu9xd+vmkIPoqH", + "NejsFZzkd1xuCn1TqGTT7ANhAEnbI/yaVt3caI30kwUC2WIToFpNgu7Ej0x2TteY", + "OgrLrdcC4jy1SifmKYv/v3ZZxrd/eqttmH2k588panseRwHK3LVk7xA+URhQ/bjL", + "gPM59V0uR1l+z1fmooeJPFz5WSXNObc9Jqnh45FND+U/UYHXTLSomTn7jgZFxJBK", + "veS7q6Lat7wAnYZCF2dnPmhZoJv+SKPltA8HAGsgQGWBF1p5qxV1HrAUk8kBBnG2", + "paj0w8p5UM6RpDdCuvKH7j1LiuWffn3b9Z4dgzmE7jsMmvzoQtypzIKaSxhqzvFO", + "od9V8dJdAgMBAAECggEAGGIYjOIB1rOKkDHP4ljXutI0mCRPl3FMDemiBeppoIfZ", + "G/Q3qpAKmndDt0Quwh/yfcNdvZhf1kwCCTWri/uPz5fSUIyDV3TaTRu0ZWoHaBVj", + "Hxylg+4HRZUQj+Vi50/PWr/jQmAAVMcrMfcoTl82q2ynmP/R1vM3EsXOCjTliv5B", + "XlMPRjj/9PDBH0dnnVcAPDOpflzOTL2f4HTFEMlmg9/tZBnd96J/cmfhjAv9XpFL", + "FBAFZzs5pz0rwCNSR8QZNonnK7pngVUlGDLORK58y84tGmxZhGdne3CtCWey/sJ4", + "7QF0Pe8YqWBU56926IY6DcSVBuQGZ6vMCNlU7J8D2QKBgQDXyh3t2TicM/n1QBLk", + "zLoGmVUmxUGziHgl2dnJiGDtyOAU3+yCorPgFaCie29s5qm4b0YEGxUxPIrRrEro", + "h0FfKn9xmr8CdmTPTcjJW1+M7bxxq7oBoU/QzKXgIHlpeCjjnvPJt0PcNkNTjCXv", + "shsrINh2rENoe/x79eEfM/N5eQKBgQDPkYSmYyALoNq8zq0A4BdR+F5lb5Fj5jBH", + "Jk68l6Uti+0hRbJ2d1tQTLkU+eCPQLGBl6fuc1i4K5FV7v14jWtRPdD7wxrkRi3j", + "ilqQwLBOU6Bj3FK4DvlLF+iYTuBWj2/KcxflXECmsjitKHLK6H7kFEiuJql+NAHU", + "U9EFXepLBQKBgQDQ+HCnZ1bFHiiP8m7Zl9EGlvK5SwlnPV9s+F1KJ4IGhCNM09UM", + "ZVfgR9F5yCONyIrPiyK40ylgtwqQJlOcf281I8irUXpsfg7+Gou5Q31y0r9NLUpC", + "Td8niyePtqMdGjouxD2+OHXFCd+FRxFt4IMi7vnxYr0csAVAXkqWlw7PsQKBgH/G", + "/PnQm7GM3BrOwAGB8dksJDAddkshMScblezTDYP0V43b8firkTLliCo5iNum357/", + "VQmdSEhXyag07yR/Kklg3H2fpbZQ3X7tdMMXW3FcWagfwWw9C4oGtdDM/Z1Lv23J", + "XDR9je8QV4OBGul+Jl8RfYx3kG94ZIfo8Qt0vP5hAoGARjAzdCGYz42NwaUk8n94", + "W2RuKHtTV9vtjaAbfPFbZoGkT7sXNJVlrA0C+9f+H9rOTM3mX59KrjmLVzde4Vhs", + "avWMShuK4vpAiDQLU7GyABvi5CR6Ld+AT+LSzxHhVe0ASOQPNCA2SOz3RQvgPi7R", + "GDgRMUB6cL3IRVzcR0dC6cY=", + )) + .unwrap(); + + let public_key = STANDARD + .decode(concat!( + "MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEArvcXfr5pCD6KhzXo7BWc", + "5Hdcbgp9U6hk0+wDYQBJ2yP8mlbd3GiN9JMFAtliE6BaTYLuxI9Mdk7XmDoKy63X", + "AuI8tUon5imL/792Wca3f3qrbZh9pOfPKWp7HkcByty1ZO8QPlEYUP24y4DzOfVd", + "LkdZfs9X5qKHiTxc+VklzTm3PSap4eORTQ/lP1GB10y0qJk5+44GRcSQSr3ku6ui", + "2re8AJ2GQhdnZz5oWaCb/kij5bQPBwBrIEBlgRdaeasVdR6wFJPJAQZxtqWo9MPK", + "eVDOkaQ3Qrryh+49S4rln3592/WeHYM5hO47DJr86ELcqcyCmksYas7xTqHfVfHS", + "XQIDAQAB", + )) + .unwrap(); + + let private_key = AsymmetricCryptoKey::from_der(&private_key).unwrap(); + let public_key = AsymmetricPublicCryptoKey::from_der(&public_key).unwrap(); + + let plaintext = "Hello, world!"; + let encrypted = + AsymmetricEncString::encrypt_rsa2048_oaep_sha1(plaintext.as_bytes(), &public_key) + .unwrap(); + let decrypted: String = encrypted.decrypt_with_key(&private_key).unwrap(); + + assert_eq!(plaintext, decrypted); + } +} diff --git a/crates/bitwarden-crypto/src/keys/device_key.rs b/crates/bitwarden-crypto/src/keys/device_key.rs new file mode 100644 index 000000000..37bcd58f9 --- /dev/null +++ b/crates/bitwarden-crypto/src/keys/device_key.rs @@ -0,0 +1,126 @@ +use crate::{ + error::Result, AsymmetricCryptoKey, AsymmetricEncString, EncString, KeyDecryptable, + KeyEncryptable, SymmetricCryptoKey, UserKey, +}; + +/// Device Key +/// +/// Encrypts the DevicePrivateKey +/// Allows the device to decrypt the UserKey, via the DevicePrivateKey. +#[derive(Debug)] +pub struct DeviceKey(SymmetricCryptoKey); + +#[derive(Debug)] +pub struct TrustDeviceResponse { + pub device_key: DeviceKey, + /// UserKey encrypted with DevicePublicKey + pub protected_user_key: AsymmetricEncString, + /// DevicePrivateKey encrypted with [DeviceKey] + pub protected_device_private_key: EncString, + /// DevicePublicKey encrypted with [UserKey](super::UserKey) + pub protected_device_public_key: EncString, +} + +impl DeviceKey { + /// Generate a new device key + /// + /// Note: Input has to be a SymmetricCryptoKey instead of UserKey because that's what we get + /// from EncSettings. + pub fn trust_device(user_key: &SymmetricCryptoKey) -> Result { + let mut rng = rand::thread_rng(); + let device_key = DeviceKey(SymmetricCryptoKey::generate(&mut rng)); + + let device_private_key = AsymmetricCryptoKey::generate(&mut rng); + + // Encrypt both the key and mac_key of the user key + let data = user_key.to_vec(); + + let protected_user_key = + AsymmetricEncString::encrypt_rsa2048_oaep_sha1(&data, &device_private_key)?; + + let protected_device_public_key = device_private_key + .to_public_der()? + .encrypt_with_key(user_key)?; + + let protected_device_private_key = device_private_key + .to_der()? + .encrypt_with_key(&device_key.0)?; + + Ok(TrustDeviceResponse { + device_key, + protected_user_key, + protected_device_private_key, + protected_device_public_key, + }) + } + + /// Decrypt the user key using the device key + pub fn decrypt_user_key( + &self, + protected_device_private_key: EncString, + protected_user_key: AsymmetricEncString, + ) -> Result { + let device_private_key: Vec = protected_device_private_key.decrypt_with_key(&self.0)?; + let device_private_key = AsymmetricCryptoKey::from_der(device_private_key.as_slice())?; + + let mut dec: Vec = protected_user_key.decrypt_with_key(&device_private_key)?; + let user_key: SymmetricCryptoKey = dec.as_mut_slice().try_into()?; + + Ok(UserKey(user_key)) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::derive_symmetric_key; + + #[test] + fn test_trust_device() { + let key = derive_symmetric_key("test"); + + let result = DeviceKey::trust_device(&key).unwrap(); + + let decrypted = result + .device_key + .decrypt_user_key( + result.protected_device_private_key, + result.protected_user_key, + ) + .unwrap(); + + assert_eq!(key.key, decrypted.0.key); + assert_eq!(key.mac_key, decrypted.0.mac_key); + } + + #[test] + fn test_decrypt_user_key() { + // Example keys from desktop app + let user_key: &mut [u8] = &mut [ + 109, 128, 172, 147, 206, 123, 134, 95, 16, 36, 155, 113, 201, 18, 186, 230, 216, 212, + 173, 188, 74, 11, 134, 131, 137, 242, 105, 178, 105, 126, 52, 139, 248, 91, 215, 21, + 128, 91, 226, 222, 165, 67, 251, 34, 83, 81, 77, 147, 225, 76, 13, 41, 102, 45, 183, + 218, 106, 89, 254, 208, 251, 101, 130, 10, + ]; + let user_key = SymmetricCryptoKey::try_from(user_key).unwrap(); + + let key_data: &mut [u8] = &mut [ + 114, 235, 60, 115, 172, 156, 203, 145, 195, 130, 215, 250, 88, 146, 215, 230, 12, 109, + 245, 222, 54, 217, 255, 211, 221, 105, 230, 236, 65, 52, 209, 133, 76, 208, 113, 254, + 194, 216, 156, 19, 230, 62, 32, 93, 87, 7, 144, 156, 117, 142, 250, 32, 182, 118, 187, + 8, 247, 7, 203, 201, 65, 147, 206, 247, + ]; + let device_key = DeviceKey(key_data.try_into().unwrap()); + + let protected_user_key: AsymmetricEncString = "4.f+VbbacRhO2q4MOUSdt1AIjQ2FuLAvg4aDxJMXAh3VxvbmUADj8Ct/R7XEpPUqApmbRS566jS0eRVy8Sk08ogoCdj1IFN9VsIky2i2X1WHK1fUnr3UBmXE3tl2NPBbx56U+h73S2jNTSyet2W18Jg2q7/w8KIhR3J41QrG9aGoOTN93to3hb5W4z6rdrSI0e7GkizbwcIA0NH7Z1JyAhrjPm9+tjRjg060YbEbGaWTAOkZWfgbLjr8bY455DteO2xxG139cOx7EBo66N+YhjsLi0ozkeUyPQkoWBdKMcQllS7jCfB4fDyJA05ALTbk74syKkvqFxqwmQbg+aVn+dcw==".parse().unwrap(); + + let protected_device_private_key: EncString = "2.GyQfUYWW6Byy4UV5icFLxg==|EMiU7OTF79N6tfv3+YUs5zJhBAgqv6sa5YCoPl6yAETh7Tfk+JmbeizxXFPj5Q1X/tcVpDZl/3fGcxtnIxg1YtvDFn7j8uPnoApOWhCKmwcvJSIkt+qvX3lELNBwZXozSiy7PbQ0JbCMe2d4MkimR5k8+lE9FB3208yYK7nOJhlrsUCnOekCYEU9/4NCMA8tz8SpITx/MN4JJ1TQ/KjPJYLt+3JNUxK47QlgREWQvyVzCRt7ZGtcgIJ/U1qycAWMpEg9NkuV8j5QRA1S7VBsA6qliJwys5+dmTuIOmOMwdKFZDc4ZvWoRkPp2TSJBu7L8sSAgU6mmDWac8iQ+9Ka/drdfwYLrH8GAZvURk79tSpRrT7+PAFe2QdUtliUIyiqkh8iJVjZube4hRnEsRuX9V9b+UdtAr6zAj7mugO/VAu5T9J38V79V2ohG3NtXysDeKLXpAlkhjllWXeq/wret2fD4WiwqEDj0G2A/PY3F3OziIgp0UKc00AfqrPq8OVK3A+aowwVqdYadgxyoVCKWJ8unJeAXG7MrMQ9tHpzF6COoaEy7Wwoc17qko33zazwLZbfAjB4oc8Ea26jRKnJZP56sVZAjOSQQMziAsA08MRaa/DQhgRea1+Ygba0gMft8Dww8anN2gQBveTZRBWyqXYgN3U0Ity5gNauT8RnFk9faqVFt2Qxnp0JgJ+PsqEt5Hn4avBRZQQ7o8VvPnxYLDKFe3I2m6HFYFWRhOGeDYxexIuaiF2iIAYFVUmnDuWpgnUiL4XJ3KHDsjkPzcV3z4D2Knr/El2VVXve8jhDjETfovmmN28+i2e29PXvKIymTskMFpFCQPc7wBY/Id7pmgb3SujKYNpkAS2sByDoRir0my49DDGfta0dENssJhFd3x+87fZbEj3cMiikg2pBwpTLgmfIUa5cVZU2s8JZ9wu7gaioYzvX+elHa3EHLcnEUoJTtSf9kjb+Nbq4ktMgYAO2wIC96t1LvmqK4Qn2cOdw5QNlRqALhqe5V31kyIcwRMK0AyIoOPhnSqtpYdFiR3LDTvZA8dU0vSsuchCwHNMeRUtKvdzN/tk+oeznyY/mpakUESN501lEKd/QFLtJZsDZTtNlcA8fU3kDtws4ZIMR0O5+PFmgQFSU8OMobf9ClUzy/wHTvYGyDuSwbOoPeS955QKkUKXCNMj33yrPr+ioHQ1BNwLX3VmMF4bNRBY/vr+CG0/EZi0Gwl0kyHGl0yWEtpQuu+/PaROJeOraWy5D1UoZZhY4n0zJZBt1eg3FZ2rhKv4gdUc50nZpeNWE8pIqZ6RQ7qPJuqfF1Z+G73iOSnLYCHDiiFmhD5ivf9IGkTAcWcBsQ/2wcSj9bFJr4DrKfsbQ4CkSWICWVn/W+InKkO6BTsBbYmvte5SvbaN+UOtiUSkHLBCCr8273VNgcB/hgtbUires3noxYZJxoczr+i7vdlEgQnWEKrpo0CifsFxGwYS3Yy2K79iwvDMaLPDf73zLSbuoUl6602F2Mzcjnals67f+gSpaDvWt7Kg9c/ZfGjq8oNxVaXJnX3gSDsO+fhwVAtnDApL+tL8cFfxGerW4KGi9/74woH+C3MMIViBtNnrpEuvxUW97Dg5nd40oGDeyi/q+8HdcxkneyFY=|JYdol19Yi+n1r7M+06EwK5JCi2s/CWqKui2Cy6hEb3k=".parse().unwrap(); + + let decrypted = device_key + .decrypt_user_key(protected_device_private_key, protected_user_key) + .unwrap(); + + assert_eq!(decrypted.0.key, user_key.key); + assert_eq!(decrypted.0.mac_key, user_key.mac_key); + } +} diff --git a/crates/bitwarden-crypto/src/keys/key_encryptable.rs b/crates/bitwarden-crypto/src/keys/key_encryptable.rs new file mode 100644 index 000000000..852b55c62 --- /dev/null +++ b/crates/bitwarden-crypto/src/keys/key_encryptable.rs @@ -0,0 +1,81 @@ +use std::{collections::HashMap, hash::Hash}; + +use crate::error::Result; + +pub trait CryptoKey {} + +pub trait KeyEncryptable { + fn encrypt_with_key(self, key: &Key) -> Result; +} + +pub trait KeyDecryptable { + fn decrypt_with_key(&self, key: &Key) -> Result; +} + +impl, Key: CryptoKey, Output> KeyEncryptable> + for Option +{ + fn encrypt_with_key(self, key: &Key) -> Result> { + self.map(|e| e.encrypt_with_key(key)).transpose() + } +} + +impl, Key: CryptoKey, Output> KeyDecryptable> + for Option +{ + fn decrypt_with_key(&self, key: &Key) -> Result> { + self.as_ref().map(|e| e.decrypt_with_key(key)).transpose() + } +} + +impl, Key: CryptoKey, Output> KeyEncryptable + for Box +{ + fn encrypt_with_key(self, key: &Key) -> Result { + (*self).encrypt_with_key(key) + } +} + +impl, Key: CryptoKey, Output> KeyDecryptable + for Box +{ + fn decrypt_with_key(&self, key: &Key) -> Result { + (**self).decrypt_with_key(key) + } +} + +impl, Key: CryptoKey, Output> KeyEncryptable> + for Vec +{ + fn encrypt_with_key(self, key: &Key) -> Result> { + self.into_iter().map(|e| e.encrypt_with_key(key)).collect() + } +} + +impl, Key: CryptoKey, Output> KeyDecryptable> + for Vec +{ + fn decrypt_with_key(&self, key: &Key) -> Result> { + self.iter().map(|e| e.decrypt_with_key(key)).collect() + } +} + +impl, Key: CryptoKey, Output, Id: Hash + Eq> + KeyEncryptable> for HashMap +{ + fn encrypt_with_key(self, key: &Key) -> Result> { + self.into_iter() + .map(|(id, e)| Ok((id, e.encrypt_with_key(key)?))) + .collect() + } +} + +impl, Key: CryptoKey, Output, Id: Hash + Eq + Copy> + KeyDecryptable> for HashMap +{ + fn decrypt_with_key(&self, key: &Key) -> Result> { + self.iter() + .map(|(id, e)| Ok((*id, e.decrypt_with_key(key)?))) + .collect() + } +} diff --git a/crates/bitwarden/src/crypto/master_key.rs b/crates/bitwarden-crypto/src/keys/master_key.rs similarity index 69% rename from crates/bitwarden/src/crypto/master_key.rs rename to crates/bitwarden-crypto/src/keys/master_key.rs index e259ec334..920f103e4 100644 --- a/crates/bitwarden/src/crypto/master_key.rs +++ b/crates/bitwarden-crypto/src/keys/master_key.rs @@ -1,14 +1,30 @@ -use aes::cipher::{generic_array::GenericArray, typenum::U32}; +use std::{num::NonZeroU32, pin::Pin}; + +use aes::cipher::typenum::U32; use base64::{engine::general_purpose::STANDARD, Engine}; -use rand::Rng; +use generic_array::GenericArray; use schemars::JsonSchema; +use serde::{Deserialize, Serialize}; use sha2::Digest; -use super::{ - hkdf_expand, EncString, KeyDecryptable, PbkdfSha256Hmac, SymmetricCryptoKey, UserKey, - PBKDF_SHA256_HMAC_OUT_SIZE, +use crate::{ + util::{self, hkdf_expand}, + EncString, KeyDecryptable, Result, SymmetricCryptoKey, UserKey, }; -use crate::{client::kdf::Kdf, error::Result}; + +#[derive(Serialize, Deserialize, Debug, JsonSchema, Clone)] +#[serde(rename_all = "camelCase", deny_unknown_fields)] +#[cfg_attr(feature = "mobile", derive(uniffi::Enum))] +pub enum Kdf { + PBKDF2 { + iterations: NonZeroU32, + }, + Argon2id { + iterations: NonZeroU32, + memory: NonZeroU32, + parallelism: NonZeroU32, + }, +} #[derive(Copy, Clone, JsonSchema)] #[cfg_attr(feature = "mobile", derive(uniffi::Enum))] @@ -17,8 +33,10 @@ pub enum HashPurpose { LocalAuthorization = 2, } -/// A Master Key. -pub(crate) struct MasterKey(SymmetricCryptoKey); +/// Master Key. +/// +/// Derived from the users master password, used to protect the [UserKey]. +pub struct MasterKey(SymmetricCryptoKey); impl MasterKey { /// Derives a users master key from their password, email and KDF. @@ -26,40 +44,33 @@ impl MasterKey { derive_key(password, email, kdf).map(Self) } - /// Derive the master key hash, used for server authorization. - pub(crate) fn derive_master_key_hash( - &self, - password: &[u8], - purpose: HashPurpose, - ) -> Result { - let hash = pbkdf2::pbkdf2_array::( - &self.0.key, - password, - purpose as u32, - ) - .expect("hash is a valid fixed size"); + /// Derive the master key hash, used for local and remote password validation. + pub fn derive_master_key_hash(&self, password: &[u8], purpose: HashPurpose) -> Result { + let hash = util::pbkdf2(&self.0.key, password, purpose as u32); Ok(STANDARD.encode(hash)) } - pub(crate) fn make_user_key(&self) -> Result<(UserKey, EncString)> { + /// Generate a new random user key and encrypt it with the master key. + pub fn make_user_key(&self) -> Result<(UserKey, EncString)> { make_user_key(rand::thread_rng(), self) } - pub(crate) fn decrypt_user_key(&self, user_key: EncString) -> Result { + /// Decrypt the users user key + pub fn decrypt_user_key(&self, user_key: EncString) -> Result { let stretched_key = stretch_master_key(self)?; - let dec: Vec = user_key.decrypt_with_key(&stretched_key)?; - SymmetricCryptoKey::try_from(dec.as_slice()) + let mut dec: Vec = user_key.decrypt_with_key(&stretched_key)?; + SymmetricCryptoKey::try_from(dec.as_mut_slice()) } - pub(crate) fn encrypt_user_key(&self, user_key: &SymmetricCryptoKey) -> Result { + pub fn encrypt_user_key(&self, user_key: &SymmetricCryptoKey) -> Result { let stretched_key = stretch_master_key(self)?; EncString::encrypt_aes256_hmac( user_key.to_vec().as_slice(), - stretched_key.mac_key.unwrap(), - stretched_key.key, + stretched_key.mac_key.as_ref().unwrap(), + &stretched_key.key, ) } } @@ -69,22 +80,15 @@ fn make_user_key( mut rng: impl rand::RngCore, master_key: &MasterKey, ) -> Result<(UserKey, EncString)> { - let mut user_key = [0u8; 64]; - rng.fill(&mut user_key); - - let user_key = SymmetricCryptoKey::try_from(user_key.as_slice())?; + let user_key = SymmetricCryptoKey::generate(&mut rng); let protected = master_key.encrypt_user_key(&user_key)?; Ok((UserKey::new(user_key), protected)) } /// Derive a generic key from a secret and salt using the provided KDF. fn derive_key(secret: &[u8], salt: &[u8], kdf: &Kdf) -> Result { - let hash = match kdf { - Kdf::PBKDF2 { iterations } => pbkdf2::pbkdf2_array::< - PbkdfSha256Hmac, - PBKDF_SHA256_HMAC_OUT_SIZE, - >(secret, salt, iterations.get()) - .unwrap(), + let mut hash = match kdf { + Kdf::PBKDF2 { iterations } => crate::util::pbkdf2(secret, salt, iterations.get()), Kdf::Argon2id { iterations, @@ -114,17 +118,13 @@ fn derive_key(secret: &[u8], salt: &[u8], kdf: &Kdf) -> Result Result { - let key: GenericArray = hkdf_expand(&master_key.0.key, Some("enc"))?; - let mac_key: GenericArray = hkdf_expand(&master_key.0.key, Some("mac"))?; - - Ok(SymmetricCryptoKey { - key, - mac_key: Some(mac_key), - }) + let key: Pin>> = hkdf_expand(&master_key.0.key, Some("enc"))?; + let mac_key: Pin>> = hkdf_expand(&master_key.0.key, Some("mac"))?; + Ok(SymmetricCryptoKey::new(key, Some(mac_key))) } #[cfg(test)] @@ -133,8 +133,8 @@ mod tests { use rand::SeedableRng; - use super::{make_user_key, stretch_master_key, HashPurpose, MasterKey}; - use crate::{client::kdf::Kdf, crypto::SymmetricCryptoKey}; + use super::{make_user_key, stretch_master_key, HashPurpose, Kdf, MasterKey}; + use crate::{keys::symmetric_crypto_key::derive_symmetric_key, SymmetricCryptoKey}; #[test] fn test_master_key_derive_pbkdf2() { @@ -182,14 +182,16 @@ mod tests { #[test] fn test_stretch_master_key() { - let master_key = MasterKey(SymmetricCryptoKey { - key: [ - 31, 79, 104, 226, 150, 71, 177, 90, 194, 80, 172, 209, 17, 129, 132, 81, 138, 167, - 69, 167, 254, 149, 2, 27, 39, 197, 64, 42, 22, 195, 86, 75, - ] - .into(), - mac_key: None, - }); + let master_key = MasterKey(SymmetricCryptoKey::new( + Box::pin( + [ + 31, 79, 104, 226, 150, 71, 177, 90, 194, 80, 172, 209, 17, 129, 132, 81, 138, + 167, 69, 167, 254, 149, 2, 27, 39, 197, 64, 42, 22, 195, 86, 75, + ] + .into(), + ), + None, + )); let stretched = stretch_master_key(&master_key).unwrap(); @@ -205,7 +207,7 @@ mod tests { 221, 127, 206, 234, 101, 27, 202, 38, 86, 52, 34, 28, 78, 28, 185, 16, 48, 61, 127, 166, 209, 247, 194, 87, 232, 26, 48, 85, 193, 249, 179, 155 ], - stretched.mac_key.unwrap().as_slice() + stretched.mac_key.as_ref().unwrap().as_slice() ); } @@ -251,14 +253,16 @@ mod tests { fn test_make_user_key() { let mut rng = rand_chacha::ChaCha8Rng::from_seed([0u8; 32]); - let master_key = MasterKey(SymmetricCryptoKey { - key: [ - 31, 79, 104, 226, 150, 71, 177, 90, 194, 80, 172, 209, 17, 129, 132, 81, 138, 167, - 69, 167, 254, 149, 2, 27, 39, 197, 64, 42, 22, 195, 86, 75, - ] - .into(), - mac_key: None, - }); + let master_key = MasterKey(SymmetricCryptoKey::new( + Box::pin( + [ + 31, 79, 104, 226, 150, 71, 177, 90, 194, 80, 172, 209, 17, 129, 132, 81, 138, + 167, 69, 167, 254, 149, 2, 27, 39, 197, 64, 42, 22, 195, 86, 75, + ] + .into(), + ), + None, + )); let (user_key, protected) = make_user_key(&mut rng, &master_key).unwrap(); @@ -270,7 +274,7 @@ mod tests { ] ); assert_eq!( - user_key.0.mac_key.unwrap().as_slice(), + user_key.0.mac_key.as_ref().unwrap().as_slice(), [ 152, 76, 225, 114, 185, 33, 111, 65, 159, 68, 83, 103, 69, 109, 86, 25, 49, 74, 66, 163, 218, 134, 176, 1, 56, 123, 253, 184, 14, 12, 254, 66 @@ -292,9 +296,9 @@ mod tests { #[test] fn test_make_user_key2() { - let master_key = MasterKey(SymmetricCryptoKey::generate("test1")); + let master_key = MasterKey(derive_symmetric_key("test1")); - let user_key = SymmetricCryptoKey::generate("test2"); + let user_key = derive_symmetric_key("test2"); let encrypted = master_key.encrypt_user_key(&user_key).unwrap(); let decrypted = master_key.decrypt_user_key(encrypted).unwrap(); diff --git a/crates/bitwarden-crypto/src/keys/mod.rs b/crates/bitwarden-crypto/src/keys/mod.rs new file mode 100644 index 000000000..285e58b55 --- /dev/null +++ b/crates/bitwarden-crypto/src/keys/mod.rs @@ -0,0 +1,19 @@ +mod key_encryptable; +pub use key_encryptable::{KeyDecryptable, KeyEncryptable}; +mod master_key; +pub use master_key::{HashPurpose, Kdf, MasterKey}; +mod shareable_key; +pub use shareable_key::derive_shareable_key; +mod symmetric_crypto_key; +#[cfg(test)] +pub use symmetric_crypto_key::derive_symmetric_key; +pub use symmetric_crypto_key::SymmetricCryptoKey; +mod asymmetric_crypto_key; +pub use asymmetric_crypto_key::{ + AsymmetricCryptoKey, AsymmetricEncryptable, AsymmetricPublicCryptoKey, +}; + +mod user_key; +pub use user_key::UserKey; +mod device_key; +pub use device_key::{DeviceKey, TrustDeviceResponse}; diff --git a/crates/bitwarden/src/crypto/shareable_key.rs b/crates/bitwarden-crypto/src/keys/shareable_key.rs similarity index 75% rename from crates/bitwarden/src/crypto/shareable_key.rs rename to crates/bitwarden-crypto/src/keys/shareable_key.rs index 4127b0712..fbb2d2e44 100644 --- a/crates/bitwarden/src/crypto/shareable_key.rs +++ b/crates/bitwarden-crypto/src/keys/shareable_key.rs @@ -1,29 +1,33 @@ -use aes::cipher::{generic_array::GenericArray, typenum::U64}; +use std::pin::Pin; + +use aes::cipher::typenum::U64; +use generic_array::GenericArray; use hmac::{Hmac, Mac}; -use crate::crypto::{hkdf_expand, SymmetricCryptoKey}; +use crate::{keys::SymmetricCryptoKey, util::hkdf_expand}; /// Derive a shareable key using hkdf from secret and name. /// /// A specialized variant of this function was called `CryptoService.makeSendKey` in the Bitwarden /// `clients` repository. -pub(crate) fn derive_shareable_key( +pub fn derive_shareable_key( secret: [u8; 16], name: &str, info: Option<&str>, ) -> SymmetricCryptoKey { // Because all inputs are fixed size, we can unwrap all errors here without issue - // TODO: Are these the final `key` and `info` parameters or should we change them? I followed the pattern used for sends + // TODO: Are these the final `key` and `info` parameters or should we change them? I followed + // the pattern used for sends let res = Hmac::::new_from_slice(format!("bitwarden-{}", name).as_bytes()) .unwrap() .chain_update(secret) .finalize() .into_bytes(); - let key: GenericArray = hkdf_expand(&res, info).unwrap(); + let mut key: Pin>> = hkdf_expand(&res, info).unwrap(); - SymmetricCryptoKey::try_from(key.as_slice()).unwrap() + SymmetricCryptoKey::try_from(key.as_mut_slice()).unwrap() } #[cfg(test)] diff --git a/crates/bitwarden-crypto/src/keys/symmetric_crypto_key.rs b/crates/bitwarden-crypto/src/keys/symmetric_crypto_key.rs new file mode 100644 index 000000000..0c165bb40 --- /dev/null +++ b/crates/bitwarden-crypto/src/keys/symmetric_crypto_key.rs @@ -0,0 +1,156 @@ +use std::{pin::Pin, str::FromStr}; + +use aes::cipher::typenum::U32; +use base64::{engine::general_purpose::STANDARD, Engine}; +use generic_array::GenericArray; +use rand::Rng; +use zeroize::{Zeroize, Zeroizing}; + +use super::key_encryptable::CryptoKey; +use crate::CryptoError; + +/// A symmetric encryption key. Used to encrypt and decrypt [`EncString`](crate::EncString) +pub struct SymmetricCryptoKey { + // GenericArray is equivalent to [u8; N], which is a Copy type placed on the stack. + // To keep the compiler from making stack copies when moving this struct around, + // we use a Box to keep the values on the heap. We also pin the box to make sure + // that the contents can't be pulled out of the box and moved + pub(crate) key: Pin>>, + pub(crate) mac_key: Option>>>, +} + +impl Drop for SymmetricCryptoKey { + fn drop(&mut self) { + self.key.zeroize(); + if let Some(mac_key) = &mut self.mac_key { + mac_key.zeroize(); + } + } +} + +impl zeroize::ZeroizeOnDrop for SymmetricCryptoKey {} + +impl SymmetricCryptoKey { + const KEY_LEN: usize = 32; + const MAC_LEN: usize = 32; + + /// Generate a new random [SymmetricCryptoKey] + pub fn generate(mut rng: impl rand::RngCore) -> Self { + let mut key = Box::pin(GenericArray::::default()); + let mut mac_key = Box::pin(GenericArray::::default()); + + rng.fill(key.as_mut_slice()); + rng.fill(mac_key.as_mut_slice()); + + SymmetricCryptoKey { + key, + mac_key: Some(mac_key), + } + } + + pub(crate) fn new( + key: Pin>>, + mac_key: Option>>>, + ) -> Self { + Self { key, mac_key } + } + + fn total_len(&self) -> usize { + self.key.len() + self.mac_key.as_ref().map_or(0, |mac| mac.len()) + } + + pub fn to_base64(&self) -> String { + let mut buf = self.to_vec(); + + let result = STANDARD.encode(&buf); + buf.zeroize(); + result + } + + pub fn to_vec(&self) -> Zeroizing> { + let mut buf = Vec::with_capacity(self.total_len()); + + buf.extend_from_slice(&self.key); + if let Some(mac) = &self.mac_key { + buf.extend_from_slice(mac); + } + Zeroizing::new(buf) + } +} + +impl FromStr for SymmetricCryptoKey { + type Err = CryptoError; + + fn from_str(s: &str) -> Result { + let mut bytes = STANDARD.decode(s).map_err(|_| CryptoError::InvalidKey)?; + SymmetricCryptoKey::try_from(bytes.as_mut_slice()) + } +} + +impl TryFrom<&mut [u8]> for SymmetricCryptoKey { + type Error = CryptoError; + + /// Note: This function takes the byte slice by mutable reference and will zero out all + /// the data in it. This is to prevent the key from being left in memory. + fn try_from(value: &mut [u8]) -> Result { + let result = if value.len() == Self::KEY_LEN + Self::MAC_LEN { + let mut key = Box::pin(GenericArray::::default()); + let mut mac_key = Box::pin(GenericArray::::default()); + + key.copy_from_slice(&value[..Self::KEY_LEN]); + mac_key.copy_from_slice(&value[Self::KEY_LEN..]); + + Ok(SymmetricCryptoKey { + key, + mac_key: Some(mac_key), + }) + } else if value.len() == Self::KEY_LEN { + let mut key = Box::pin(GenericArray::::default()); + + key.copy_from_slice(&value[..Self::KEY_LEN]); + + Ok(SymmetricCryptoKey { key, mac_key: None }) + } else { + Err(CryptoError::InvalidKeyLen) + }; + + value.zeroize(); + result + } +} + +impl CryptoKey for SymmetricCryptoKey {} + +// We manually implement these to make sure we don't print any sensitive data +impl std::fmt::Debug for SymmetricCryptoKey { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("SymmetricCryptoKey").finish() + } +} + +#[cfg(test)] +pub fn derive_symmetric_key(name: &str) -> SymmetricCryptoKey { + use crate::{derive_shareable_key, generate_random_bytes}; + + let secret: [u8; 16] = generate_random_bytes(); + derive_shareable_key(secret, name, None) +} + +#[cfg(test)] +mod tests { + use std::str::FromStr; + + use super::{derive_symmetric_key, SymmetricCryptoKey}; + + #[test] + fn test_symmetric_crypto_key() { + let key = derive_symmetric_key("test"); + let key2 = SymmetricCryptoKey::from_str(&key.to_base64()).unwrap(); + assert_eq!(key.key, key2.key); + assert_eq!(key.mac_key, key2.mac_key); + + let key = "UY4B5N4DA4UisCNClgZtRr6VLy9ZF5BXXC7cDZRqourKi4ghEMgISbCsubvgCkHf5DZctQjVot11/vVvN9NNHQ=="; + let key2 = SymmetricCryptoKey::from_str(key).unwrap(); + assert_eq!(key, key2.to_base64()); + } +} diff --git a/crates/bitwarden-crypto/src/keys/user_key.rs b/crates/bitwarden-crypto/src/keys/user_key.rs new file mode 100644 index 000000000..6620236ec --- /dev/null +++ b/crates/bitwarden-crypto/src/keys/user_key.rs @@ -0,0 +1,19 @@ +use crate::{ + rsa::{make_key_pair, RsaKeyPair}, + Result, SymmetricCryptoKey, +}; + +/// User Key +/// +/// The User Key is the symmetric encryption key used to decrypt the user's vault. +pub struct UserKey(pub SymmetricCryptoKey); + +impl UserKey { + pub fn new(key: SymmetricCryptoKey) -> Self { + Self(key) + } + + pub fn make_key_pair(&self) -> Result { + make_key_pair(&self.0) + } +} diff --git a/crates/bitwarden-crypto/src/lib.rs b/crates/bitwarden-crypto/src/lib.rs new file mode 100644 index 000000000..3c1aced24 --- /dev/null +++ b/crates/bitwarden-crypto/src/lib.rs @@ -0,0 +1,48 @@ +//! # Bitwarden Cryptographic primitives +//! +//! This crate contains the cryptographic primitives used throughout the SDK. The crate makes a +//! best effort to abstract away cryptographic concepts into concepts such as [`EncString`], +//! [`AsymmetricEncString`] and [`SymmetricCryptoKey`]. +//! +//! ## Conventions: +//! +//! - Pure Functions that deterministically "derive" keys from input are prefixed with `derive_`. +//! - Functions that generate non deterministically keys are prefixed with `make_`. +//! +//! ## Differences from `clients` +//! +//! There are some noteworthy differences compared to the other Bitwarden +//! [clients](https://github.com/bitwarden/clients). These changes are made in an effort to +//! introduce conventions in how we name things, improve best practices and abstracting away +//! internal complexity. +//! +//! - `CryptoService.makeSendKey` & `AccessService.createAccessToken` are replaced by the generic +//! `derive_shareable_key` +//! - MasterKey operations such as `makeMasterKey` and `hashMasterKey` are moved to the MasterKey +//! struct. + +mod aes; +mod enc_string; +pub use enc_string::{AsymmetricEncString, EncString}; +mod encryptable; +pub use encryptable::{Decryptable, Encryptable, KeyContainer, LocateKey}; +mod error; +pub use error::CryptoError; +pub(crate) use error::Result; +mod fingerprint; +pub use fingerprint::fingerprint; +mod keys; +pub use keys::*; +mod rsa; +pub use crate::rsa::RsaKeyPair; +mod util; +pub use util::generate_random_bytes; +mod wordlist; +pub use util::pbkdf2; +pub use wordlist::EFF_LONG_WORD_LIST; + +#[cfg(feature = "mobile")] +uniffi::setup_scaffolding!(); + +#[cfg(feature = "mobile")] +mod uniffi_support; diff --git a/crates/bitwarden/src/crypto/rsa.rs b/crates/bitwarden-crypto/src/rsa.rs similarity index 50% rename from crates/bitwarden/src/crypto/rsa.rs rename to crates/bitwarden-crypto/src/rsa.rs index 9641237fc..231e77aaa 100644 --- a/crates/bitwarden/src/crypto/rsa.rs +++ b/crates/bitwarden-crypto/src/rsa.rs @@ -1,14 +1,18 @@ use base64::{engine::general_purpose::STANDARD, Engine}; use rsa::{ pkcs8::{EncodePrivateKey, EncodePublicKey}, - RsaPrivateKey, RsaPublicKey, + Oaep, RsaPrivateKey, RsaPublicKey, }; +use sha1::Sha1; use crate::{ - crypto::{EncString, SymmetricCryptoKey}, - error::Result, + error::{Result, RsaError}, + CryptoError, EncString, SymmetricCryptoKey, }; +/// RSA Key Pair +/// +/// Consists of a public key and an encrypted private key. #[cfg_attr(feature = "mobile", derive(uniffi::Record))] pub struct RsaKeyPair { /// Base64 encoded DER representation of the public key @@ -17,7 +21,7 @@ pub struct RsaKeyPair { pub private: EncString, } -pub(super) fn make_key_pair(key: &SymmetricCryptoKey) -> Result { +pub(crate) fn make_key_pair(key: &SymmetricCryptoKey) -> Result { let mut rng = rand::thread_rng(); let bits = 2048; let priv_key = RsaPrivateKey::new(&mut rng, bits).expect("failed to generate a key"); @@ -25,17 +29,27 @@ pub(super) fn make_key_pair(key: &SymmetricCryptoKey) -> Result { let spki = pub_key .to_public_key_der() - .map_err(|_| "unable to create public key")?; + .map_err(|_| RsaError::CreatePublicKey)?; let b64 = STANDARD.encode(spki.as_bytes()); let pkcs = priv_key .to_pkcs8_der() - .map_err(|_| "unable to create private key")?; + .map_err(|_| RsaError::CreatePrivateKey)?; - let protected = EncString::encrypt_aes256_hmac(pkcs.as_bytes(), key.mac_key.unwrap(), key.key)?; + let protected = + EncString::encrypt_aes256_hmac(pkcs.as_bytes(), key.mac_key.as_ref().unwrap(), &key.key)?; Ok(RsaKeyPair { public: b64, private: protected, }) } + +pub(super) fn encrypt_rsa2048_oaep_sha1(public_key: &RsaPublicKey, data: &[u8]) -> Result> { + let mut rng = rand::thread_rng(); + + let padding = Oaep::new::(); + public_key + .encrypt(&mut rng, padding, data) + .map_err(|e| CryptoError::RsaError(e.into())) +} diff --git a/crates/bitwarden-crypto/src/uniffi_support.rs b/crates/bitwarden-crypto/src/uniffi_support.rs new file mode 100644 index 000000000..0ff0194f9 --- /dev/null +++ b/crates/bitwarden-crypto/src/uniffi_support.rs @@ -0,0 +1,31 @@ +use std::num::NonZeroU32; + +use crate::{CryptoError, EncString, UniffiCustomTypeConverter}; + +uniffi::custom_type!(NonZeroU32, u32); + +impl UniffiCustomTypeConverter for NonZeroU32 { + type Builtin = u32; + + fn into_custom(val: Self::Builtin) -> uniffi::Result { + Self::new(val).ok_or(CryptoError::ZeroNumber.into()) + } + + fn from_custom(obj: Self) -> Self::Builtin { + obj.get() + } +} + +uniffi::custom_type!(EncString, String); + +impl UniffiCustomTypeConverter for EncString { + type Builtin = String; + + fn into_custom(val: Self::Builtin) -> uniffi::Result { + val.parse().map_err(|e: CryptoError| e.into()) + } + + fn from_custom(obj: Self) -> Self::Builtin { + obj.to_string() + } +} diff --git a/crates/bitwarden-crypto/src/util.rs b/crates/bitwarden-crypto/src/util.rs new file mode 100644 index 000000000..ba60db366 --- /dev/null +++ b/crates/bitwarden-crypto/src/util.rs @@ -0,0 +1,70 @@ +use std::pin::Pin; + +use ::aes::cipher::{ArrayLength, Unsigned}; +use generic_array::GenericArray; +use hmac::digest::OutputSizeUser; +use rand::{ + distributions::{Distribution, Standard}, + Rng, +}; + +use crate::{CryptoError, Result}; + +pub(crate) type PbkdfSha256Hmac = hmac::Hmac; +pub(crate) const PBKDF_SHA256_HMAC_OUT_SIZE: usize = + <::OutputSize as Unsigned>::USIZE; + +/// [RFC5869](https://datatracker.ietf.org/doc/html/rfc5869) HKDF-Expand operation +pub(crate) fn hkdf_expand>( + prk: &[u8], + info: Option<&str>, +) -> Result>>> { + let hkdf = hkdf::Hkdf::::from_prk(prk).map_err(|_| CryptoError::InvalidKeyLen)?; + let mut key = Box::>::default(); + + let i = info.map(|i| i.as_bytes()).unwrap_or(&[]); + hkdf.expand(i, &mut key) + .map_err(|_| CryptoError::InvalidKeyLen)?; + + Ok(Box::into_pin(key)) +} + +/// Generate random bytes that are cryptographically secure +pub fn generate_random_bytes() -> T +where + Standard: Distribution, +{ + rand::thread_rng().gen() +} + +pub fn pbkdf2(password: &[u8], salt: &[u8], rounds: u32) -> [u8; PBKDF_SHA256_HMAC_OUT_SIZE] { + pbkdf2::pbkdf2_array::(password, salt, rounds) + .expect("hash is a valid fixed size") +} + +#[cfg(test)] +mod tests { + use aes::cipher::typenum::U64; + + use super::*; + + #[test] + fn test_hkdf_expand() { + let prk = &[ + 23, 152, 120, 41, 214, 16, 156, 133, 71, 226, 178, 135, 208, 255, 66, 101, 189, 70, + 173, 30, 39, 215, 175, 236, 38, 180, 180, 62, 196, 4, 159, 70, + ]; + let info = Some("info"); + + let result: Pin>> = hkdf_expand(prk, info).unwrap(); + + let expected_output: [u8; 64] = [ + 6, 114, 42, 38, 87, 231, 30, 109, 30, 255, 104, 129, 255, 94, 92, 108, 124, 145, 215, + 208, 17, 60, 135, 22, 70, 158, 40, 53, 45, 182, 8, 63, 65, 87, 239, 234, 185, 227, 153, + 122, 115, 205, 144, 56, 102, 149, 92, 139, 217, 102, 119, 57, 37, 57, 251, 178, 18, 52, + 94, 77, 132, 215, 239, 100, + ]; + + assert_eq!(result.as_slice(), expected_output); + } +} diff --git a/crates/bitwarden/src/wordlist.rs b/crates/bitwarden-crypto/src/wordlist.rs similarity index 99% rename from crates/bitwarden/src/wordlist.rs rename to crates/bitwarden-crypto/src/wordlist.rs index 4974969e6..4cc30ff74 100644 --- a/crates/bitwarden/src/wordlist.rs +++ b/crates/bitwarden-crypto/src/wordlist.rs @@ -1,6 +1,5 @@ // EFF's Long Wordlist from https://www.eff.org/dice -#[cfg(feature = "internal")] -pub(crate) const EFF_LONG_WORD_LIST: &[&str] = &[ +pub const EFF_LONG_WORD_LIST: &[&str] = &[ "abacus", "abdomen", "abdominal", diff --git a/crates/bitwarden-crypto/uniffi.toml b/crates/bitwarden-crypto/uniffi.toml new file mode 100644 index 000000000..3cac32ecc --- /dev/null +++ b/crates/bitwarden-crypto/uniffi.toml @@ -0,0 +1,9 @@ +[bindings.kotlin] +package_name = "com.bitwarden.crypto" +generate_immutable_records = true +android = true + +[bindings.swift] +ffi_module_name = "BitwardenCryptoFFI" +module_name = "BitwardenCrypto" +generate_immutable_records = true diff --git a/crates/bitwarden-generators/Cargo.toml b/crates/bitwarden-generators/Cargo.toml new file mode 100644 index 000000000..7f26a47cf --- /dev/null +++ b/crates/bitwarden-generators/Cargo.toml @@ -0,0 +1,33 @@ +[package] +name = "bitwarden-generators" +version = "0.1.0" +authors = ["Bitwarden Inc"] +license-file = "LICENSE" +repository = "https://github.com/bitwarden/sdk" +homepage = "https://bitwarden.com" +description = """ +Internal crate for the bitwarden crate. Do not use. +""" +keywords = ["bitwarden"] +edition = "2021" +rust-version = "1.57" + +[features] +mobile = ["uniffi"] # Mobile-specific features + +[dependencies] +bitwarden-crypto = { path = "../bitwarden-crypto", version = "=0.1.0" } +rand = ">=0.8.5, <0.9" +reqwest = { version = ">=0.11, <0.12", features = [ + "json", +], default-features = false } +schemars = { version = ">=0.8.9, <0.9", features = ["uuid1", "chrono"] } +serde = { version = ">=1.0, <2.0", features = ["derive"] } +serde_json = ">=1.0.96, <2.0" +thiserror = ">=1.0.40, <2.0" +uniffi = { version = "=0.26.1", optional = true } + +[dev-dependencies] +rand_chacha = "0.3.1" +tokio = { version = "1.35.1", features = ["rt", "macros"] } +wiremock = "0.5.22" diff --git a/crates/bitwarden-generators/README.md b/crates/bitwarden-generators/README.md new file mode 100644 index 000000000..db70c11df --- /dev/null +++ b/crates/bitwarden-generators/README.md @@ -0,0 +1,6 @@ +# Bitwarden Generators + +This is an internal crate for the Bitwarden SDK do not depend on this directly and use the +[`bitwarden`](https://crates.io/crates/bitwarden) crate instead. + +This crate does not follow semantic versioning and the public interface may change at any time. diff --git a/crates/bitwarden-generators/src/lib.rs b/crates/bitwarden-generators/src/lib.rs new file mode 100644 index 000000000..335ec92b9 --- /dev/null +++ b/crates/bitwarden-generators/src/lib.rs @@ -0,0 +1,11 @@ +mod passphrase; +pub use passphrase::{passphrase, PassphraseError, PassphraseGeneratorRequest}; +mod password; +mod util; +pub use password::{password, PasswordError, PasswordGeneratorRequest}; +mod username; +pub use username::{username, ForwarderServiceType, UsernameError, UsernameGeneratorRequest}; +mod username_forwarders; + +#[cfg(feature = "mobile")] +uniffi::setup_scaffolding!(); diff --git a/crates/bitwarden/src/tool/generators/passphrase.rs b/crates/bitwarden-generators/src/passphrase.rs similarity index 85% rename from crates/bitwarden/src/tool/generators/passphrase.rs rename to crates/bitwarden-generators/src/passphrase.rs index 7c5c8e434..f8329a80c 100644 --- a/crates/bitwarden/src/tool/generators/passphrase.rs +++ b/crates/bitwarden-generators/src/passphrase.rs @@ -1,7 +1,18 @@ -use crate::{error::Result, util::capitalize_first_letter, wordlist::EFF_LONG_WORD_LIST}; +use bitwarden_crypto::EFF_LONG_WORD_LIST; use rand::{seq::SliceRandom, Rng, RngCore}; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; +use thiserror::Error; + +use crate::util::capitalize_first_letter; + +#[derive(Debug, Error)] +pub enum PassphraseError { + #[error("'num_words' must be between {} and {}", minimum, maximum)] + InvalidNumWords { minimum: u8, maximum: u8 }, + #[error("'word_separator' cannot be empty")] + EmptyWordSeparator, +} /// Passphrase generator request options. #[derive(Serialize, Deserialize, Debug, JsonSchema)] @@ -15,7 +26,8 @@ pub struct PassphraseGeneratorRequest { 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. + /// When set to true, include a number at the end of one of the words in the generated + /// passphrase. pub include_number: bool, } @@ -34,7 +46,8 @@ 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) +/// 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, @@ -43,17 +56,21 @@ struct ValidPassphraseGeneratorOptions { } impl PassphraseGeneratorRequest { - /// Validates the request and returns an immutable struct with valid options to use with the passphrase generator. - fn validate_options(self) -> Result { + /// 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(format!("'num_words' must be between {MINIMUM_PASSPHRASE_NUM_WORDS} and {MAXIMUM_PASSPHRASE_NUM_WORDS}").into()); + return Err(PassphraseError::InvalidNumWords { + minimum: MINIMUM_PASSPHRASE_NUM_WORDS, + maximum: MAXIMUM_PASSPHRASE_NUM_WORDS, + }); } if self.word_separator.chars().next().is_none() { - return Err("'word_separator' cannot be empty".into()); + return Err(PassphraseError::EmptyWordSeparator); }; Ok(ValidPassphraseGeneratorOptions { @@ -65,13 +82,8 @@ impl PassphraseGeneratorRequest { } } -/// 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 { +/// Implementation of the random passphrase generator. +pub fn passphrase(request: PassphraseGeneratorRequest) -> Result { let options = request.validate_options()?; Ok(passphrase_with_rng(rand::thread_rng(), options)) } @@ -164,7 +176,8 @@ mod tests { let input = PassphraseGeneratorRequest { num_words: 4, - word_separator: "👨🏻‍❤️‍💋‍👨🏻".into(), // This emoji is 35 bytes long, but represented as a single character + word_separator: "👨🏻‍❤️‍💋‍👨🏻".into(), /* This emoji is 35 bytes long, but represented + * as a single character */ capitalize: false, include_number: true, } diff --git a/crates/bitwarden/src/tool/generators/password.rs b/crates/bitwarden-generators/src/password.rs similarity index 94% rename from crates/bitwarden/src/tool/generators/password.rs rename to crates/bitwarden-generators/src/password.rs index d091d1a45..6cab6dd65 100644 --- a/crates/bitwarden/src/tool/generators/password.rs +++ b/crates/bitwarden-generators/src/password.rs @@ -3,8 +3,15 @@ use std::collections::BTreeSet; use rand::{distributions::Distribution, seq::SliceRandom, RngCore}; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; - -use crate::error::Result; +use thiserror::Error; + +#[derive(Debug, Error)] +pub enum PasswordError { + #[error("No character set enabled")] + NoCharacterSetEnabled, + #[error("Invalid password length")] + InvalidLength, +} /// Password generator request options. #[derive(Serialize, Deserialize, Debug, JsonSchema)] @@ -32,7 +39,7 @@ pub struct PasswordGeneratorRequest { /// 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 + /// 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 @@ -78,7 +85,8 @@ impl CharSet { self.include_if(true, other) } - /// Includes the given characters in the set if the predicate is true. Any duplicate items will be ignored + /// 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); @@ -115,7 +123,8 @@ impl Distribution for CharSet { } /// Represents a set of valid options to generate a password with. -/// To get an instance of it, use [`PasswordGeneratorRequest::validate_options`](PasswordGeneratorRequest::validate_options) +/// 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), @@ -127,17 +136,18 @@ struct PasswordGeneratorOptions { } impl PasswordGeneratorRequest { - /// Validates the request and returns an immutable struct with valid options to use with the password generator. - fn validate_options(self) -> Result { + /// 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("At least one character set must be enabled".into()); + return Err(PasswordError::NoCharacterSetEnabled); } if self.length < 4 { - return Err("A password must be at least 4 characters long".into()); + return Err(PasswordError::InvalidLength); } // Make sure the minimum values are zero when the character @@ -159,7 +169,7 @@ impl PasswordGeneratorRequest { // 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("Password length can't be less than the sum of the minimums".into()); + return Err(PasswordError::InvalidLength); } let lower = ( @@ -208,9 +218,8 @@ impl PasswordGeneratorRequest { } } -/// 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 { +/// Implementation of the random password generator. +pub fn password(input: PasswordGeneratorRequest) -> Result { let options = input.validate_options()?; Ok(password_with_rng(rand::thread_rng(), options)) } diff --git a/crates/bitwarden/src/tool/generators/username.rs b/crates/bitwarden-generators/src/username.rs similarity index 91% rename from crates/bitwarden/src/tool/generators/username.rs rename to crates/bitwarden-generators/src/username.rs index 666772714..ccb46604b 100644 --- a/crates/bitwarden/src/tool/generators/username.rs +++ b/crates/bitwarden-generators/src/username.rs @@ -1,7 +1,25 @@ -use crate::{error::Result, util::capitalize_first_letter, wordlist::EFF_LONG_WORD_LIST}; +use bitwarden_crypto::EFF_LONG_WORD_LIST; use rand::{distributions::Distribution, seq::SliceRandom, Rng, RngCore}; +use reqwest::StatusCode; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; +use thiserror::Error; + +use crate::util::capitalize_first_letter; + +#[derive(Debug, Error)] +pub enum UsernameError { + #[error("Invalid API Key")] + InvalidApiKey, + #[error("Unknown error")] + Unknown, + + #[error("Received error message from server: [{}] {}", .status, .message)] + ResponseContent { status: StatusCode, message: String }, + + #[error(transparent)] + Reqwest(#[from] reqwest::Error), +} #[derive(Serialize, Deserialize, Debug, JsonSchema)] #[serde(rename_all = "camelCase", deny_unknown_fields)] @@ -83,10 +101,15 @@ pub enum UsernameGeneratorRequest { impl ForwarderServiceType { // Generate a username using the specified email forwarding service // This requires an HTTP client to be passed in, as the service will need to make API calls - pub async fn generate(self, http: &reqwest::Client, website: Option) -> Result { - use crate::tool::generators::username_forwarders::*; + pub async fn generate( + self, + http: &reqwest::Client, + website: Option, + ) -> Result { use ForwarderServiceType::*; + use crate::username_forwarders::*; + match self { AddyIo { api_token, @@ -104,14 +127,14 @@ impl ForwarderServiceType { } } -/// Implementation of the username generator. This is not accessible to the public API. -/// See [`ClientGenerator::username`](crate::ClientGenerator::username) for the API function. +/// Implementation of the username generator. +/// /// Note: The HTTP client is passed in as a required parameter for convenience, /// as some username generators require making API calls. -pub(super) async fn username( +pub async fn username( input: UsernameGeneratorRequest, http: &reqwest::Client, -) -> Result { +) -> Result { use rand::thread_rng; use UsernameGeneratorRequest::*; match input { diff --git a/crates/bitwarden-generators/src/username_forwarders/addyio.rs b/crates/bitwarden-generators/src/username_forwarders/addyio.rs new file mode 100644 index 000000000..4b75e1c84 --- /dev/null +++ b/crates/bitwarden-generators/src/username_forwarders/addyio.rs @@ -0,0 +1,158 @@ +use reqwest::{header::CONTENT_TYPE, StatusCode}; + +use crate::username::UsernameError; + +pub async fn generate( + http: &reqwest::Client, + api_token: String, + domain: String, + base_url: String, + website: Option, +) -> Result { + let description = super::format_description(&website); + + #[derive(serde::Serialize)] + struct Request { + domain: String, + description: String, + } + + let response = http + .post(format!("{base_url}/api/v1/aliases")) + .header(CONTENT_TYPE, "application/json") + .bearer_auth(api_token) + .header("X-Requested-With", "XMLHttpRequest") + .json(&Request { + domain, + description, + }) + .send() + .await?; + + if response.status() == StatusCode::UNAUTHORIZED { + return Err(UsernameError::InvalidApiKey); + } + + // Throw any other errors + response.error_for_status_ref()?; + + #[derive(serde::Deserialize)] + struct ResponseData { + email: String, + } + #[derive(serde::Deserialize)] + struct Response { + data: ResponseData, + } + let response: Response = response.json().await?; + + Ok(response.data.email) +} + +#[cfg(test)] +mod tests { + use serde_json::json; + + use crate::username::UsernameError; + #[tokio::test] + async fn test_mock_server() { + use wiremock::{matchers, Mock, ResponseTemplate}; + + let server = wiremock::MockServer::start().await; + + // Mock the request to the addy.io API, and verify that the correct request is made + server + .register( + Mock::given(matchers::path("/api/v1/aliases")) + .and(matchers::method("POST")) + .and(matchers::header("Content-Type", "application/json")) + .and(matchers::header("Authorization", "Bearer MY_TOKEN")) + .and(matchers::body_json(json!({ + "domain": "myemail.com", + "description": "Website: example.com. Generated by Bitwarden." + }))) + .respond_with(ResponseTemplate::new(201).set_body_json(json!({ + "data": { + "id": "50c9e585-e7f5-41c4-9016-9014c15454bc", + "user_id": "ca0a4e09-c266-4f6f-845c-958db5090f09", + "local_part": "50c9e585-e7f5-41c4-9016-9014c15454bc", + "domain": "myemail.com", + "email": "50c9e585-e7f5-41c4-9016-9014c15454bc@myemail.com", + "active": true + } + }))) + .expect(1), + ) + .await; + // Mock an invalid API token request + server + .register( + Mock::given(matchers::path("/api/v1/aliases")) + .and(matchers::method("POST")) + .and(matchers::header("Content-Type", "application/json")) + .and(matchers::header("Authorization", "Bearer MY_FAKE_TOKEN")) + .and(matchers::body_json(json!({ + "domain": "myemail.com", + "description": "Website: example.com. Generated by Bitwarden." + }))) + .respond_with(ResponseTemplate::new(401)) + .expect(1), + ) + .await; + // Mock an invalid domain + server + .register( + Mock::given(matchers::path("/api/v1/aliases")) + .and(matchers::method("POST")) + .and(matchers::header("Content-Type", "application/json")) + .and(matchers::header("Authorization", "Bearer MY_TOKEN")) + .and(matchers::body_json(json!({ + "domain": "gmail.com", + "description": "Website: example.com. Generated by Bitwarden." + }))) + .respond_with(ResponseTemplate::new(403)) + .expect(1), + ) + .await; + + let address = super::generate( + &reqwest::Client::new(), + "MY_TOKEN".into(), + "myemail.com".into(), + format!("http://{}", server.address()), + Some("example.com".into()), + ) + .await + .unwrap(); + + let fake_token_error = super::generate( + &reqwest::Client::new(), + "MY_FAKE_TOKEN".into(), + "myemail.com".into(), + format!("http://{}", server.address()), + Some("example.com".into()), + ) + .await + .unwrap_err(); + + assert_eq!( + fake_token_error.to_string(), + UsernameError::InvalidApiKey.to_string() + ); + + let fake_domain_error = super::generate( + &reqwest::Client::new(), + "MY_TOKEN".into(), + "gmail.com".into(), + format!("http://{}", server.address()), + Some("example.com".into()), + ) + .await + .unwrap_err(); + + assert!(fake_domain_error.to_string().contains("403 Forbidden")); + + server.verify().await; + assert_eq!(address, "50c9e585-e7f5-41c4-9016-9014c15454bc@myemail.com"); + } +} diff --git a/crates/bitwarden/src/tool/generators/username_forwarders/duckduckgo.rs b/crates/bitwarden-generators/src/username_forwarders/duckduckgo.rs similarity index 50% rename from crates/bitwarden/src/tool/generators/username_forwarders/duckduckgo.rs rename to crates/bitwarden-generators/src/username_forwarders/duckduckgo.rs index 512db7812..3f21fd3a5 100644 --- a/crates/bitwarden/src/tool/generators/username_forwarders/duckduckgo.rs +++ b/crates/bitwarden-generators/src/username_forwarders/duckduckgo.rs @@ -1,7 +1,8 @@ use reqwest::{header::CONTENT_TYPE, StatusCode}; -use crate::error::Result; -pub async fn generate(http: &reqwest::Client, token: String) -> Result { +use crate::username::UsernameError; + +pub async fn generate(http: &reqwest::Client, token: String) -> Result { generate_with_api_url(http, token, "https://quack.duckduckgo.com".into()).await } @@ -9,7 +10,7 @@ async fn generate_with_api_url( http: &reqwest::Client, token: String, api_url: String, -) -> Result { +) -> Result { let response = http .post(format!("{api_url}/api/email/addresses")) .header(CONTENT_TYPE, "application/json") @@ -18,7 +19,7 @@ async fn generate_with_api_url( .await?; if response.status() == StatusCode::UNAUTHORIZED { - return Err("Invalid DuckDuckGo API token".into()); + return Err(UsernameError::InvalidApiKey); } // Throw any other errors @@ -36,29 +37,38 @@ async fn generate_with_api_url( #[cfg(test)] mod tests { use serde_json::json; + + use crate::username::UsernameError; #[tokio::test] async fn test_mock_server() { use wiremock::{matchers, Mock, ResponseTemplate}; - let (server, _client) = crate::util::start_mock(vec![ - // Mock the request to the DDG API, and verify that the correct request is made - Mock::given(matchers::path("/api/email/addresses")) - .and(matchers::method("POST")) - .and(matchers::header("Content-Type", "application/json")) - .and(matchers::header("Authorization", "Bearer MY_TOKEN")) - .respond_with(ResponseTemplate::new(201).set_body_json(json!({ - "address": "bw7prt" - }))) - .expect(1), - // Mock an invalid token request - Mock::given(matchers::path("/api/email/addresses")) - .and(matchers::method("POST")) - .and(matchers::header("Content-Type", "application/json")) - .and(matchers::header("Authorization", "Bearer MY_FAKE_TOKEN")) - .respond_with(ResponseTemplate::new(401)) - .expect(1), - ]) - .await; + let server = wiremock::MockServer::start().await; + + // Mock the request to the DDG API, and verify that the correct request is made + server + .register( + Mock::given(matchers::path("/api/email/addresses")) + .and(matchers::method("POST")) + .and(matchers::header("Content-Type", "application/json")) + .and(matchers::header("Authorization", "Bearer MY_TOKEN")) + .respond_with(ResponseTemplate::new(201).set_body_json(json!({ + "address": "bw7prt" + }))) + .expect(1), + ) + .await; + // Mock an invalid token request + server + .register( + Mock::given(matchers::path("/api/email/addresses")) + .and(matchers::method("POST")) + .and(matchers::header("Content-Type", "application/json")) + .and(matchers::header("Authorization", "Bearer MY_FAKE_TOKEN")) + .respond_with(ResponseTemplate::new(401)) + .expect(1), + ) + .await; let address = super::generate_with_api_url( &reqwest::Client::new(), @@ -77,9 +87,10 @@ mod tests { .await .unwrap_err(); - assert!(fake_token_error - .to_string() - .contains("Invalid DuckDuckGo API token")); + assert_eq!( + fake_token_error.to_string(), + UsernameError::InvalidApiKey.to_string() + ); server.verify().await; } diff --git a/crates/bitwarden/src/tool/generators/username_forwarders/fastmail.rs b/crates/bitwarden-generators/src/username_forwarders/fastmail.rs similarity index 67% rename from crates/bitwarden/src/tool/generators/username_forwarders/fastmail.rs rename to crates/bitwarden-generators/src/username_forwarders/fastmail.rs index d2a7554e2..6cc63647a 100644 --- a/crates/bitwarden/src/tool/generators/username_forwarders/fastmail.rs +++ b/crates/bitwarden-generators/src/username_forwarders/fastmail.rs @@ -3,12 +3,13 @@ use std::collections::HashMap; use reqwest::{header::CONTENT_TYPE, StatusCode}; use serde_json::json; -use crate::error::Result; +use crate::username::UsernameError; + pub async fn generate( http: &reqwest::Client, api_token: String, website: Option, -) -> Result { +) -> Result { generate_with_api_url(http, api_token, website, "https://api.fastmail.com".into()).await } @@ -17,7 +18,7 @@ pub async fn generate_with_api_url( api_token: String, website: Option, api_url: String, -) -> Result { +) -> Result { let account_id = get_account_id(http, &api_token, &api_url).await?; let response = http @@ -44,13 +45,14 @@ pub async fn generate_with_api_url( .send() .await?; - if response.status() == StatusCode::UNAUTHORIZED { - return Err("Invalid Fastmail API token".into()); + let status_code = response.status(); + if status_code == StatusCode::UNAUTHORIZED { + return Err(UsernameError::InvalidApiKey); } - let response: serde_json::Value = response.json().await?; - let Some(r) = response.get("methodResponses").and_then(|r| r.get(0)) else { - return Err("Unknown Fastmail error occurred.".into()); + let response_json: serde_json::Value = response.json().await?; + let Some(r) = response_json.get("methodResponses").and_then(|r| r.get(0)) else { + return Err(UsernameError::Unknown); }; let method_response = r.get(0).and_then(|r| r.as_str()); let response_value = r.get(1); @@ -72,24 +74,30 @@ pub async fn generate_with_api_url( .and_then(|r| r.as_str()) .unwrap_or("Unknown error"); - return Err(format!("Fastmail error: {error_description}").into()); + return Err(UsernameError::ResponseContent { + status: status_code, + message: error_description.to_owned(), + }); } else if method_response == Some("error") { let error_description = response_value .and_then(|r| r.get("description")) .and_then(|r| r.as_str()) .unwrap_or("Unknown error"); - return Err(format!("Fastmail error: {error_description}").into()); + return Err(UsernameError::ResponseContent { + status: status_code, + message: error_description.to_owned(), + }); } - Err("Unknown Fastmail error occurred.".into()) + Err(UsernameError::Unknown) } async fn get_account_id( client: &reqwest::Client, api_token: &str, api_url: &str, -) -> Result { +) -> Result { #[derive(serde::Deserialize)] struct Response { #[serde(rename = "primaryAccounts")] @@ -102,7 +110,7 @@ async fn get_account_id( .await?; if response.status() == StatusCode::UNAUTHORIZED { - return Err("Invalid Fastmail API token".into()); + return Err(UsernameError::InvalidApiKey); } response.error_for_status_ref()?; @@ -117,13 +125,16 @@ async fn get_account_id( #[cfg(test)] mod tests { use serde_json::json; + + use crate::username::UsernameError; #[tokio::test] async fn test_mock_server() { use wiremock::{matchers, Mock, ResponseTemplate}; - let (server, _client) = crate::util::start_mock(vec![ - // Mock a valid request to FastMail API - Mock::given(matchers::path("/.well-known/jmap")) + let server = wiremock::MockServer::start().await; + + // Mock a valid request to FastMail API + server.register(Mock::given(matchers::path("/.well-known/jmap")) .and(matchers::method("GET")) .and(matchers::header("Authorization", "Bearer MY_TOKEN")) .respond_with(ResponseTemplate::new(201).set_body_json(json!({ @@ -131,9 +142,9 @@ mod tests { "https://www.fastmail.com/dev/maskedemail": "ca0a4e09-c266-4f6f-845c-958db5090f09" } }))) - .expect(1), + .expect(1)).await; - Mock::given(matchers::path("/jmap/api/")) + server.register(Mock::given(matchers::path("/jmap/api/")) .and(matchers::method("POST")) .and(matchers::header("Content-Type", "application/json")) .and(matchers::header("Authorization", "Bearer MY_TOKEN")) @@ -142,23 +153,29 @@ mod tests { ["MaskedEmail/set", {"created": {"new-masked-email": {"email": "9f823dq23d123ds@mydomain.com"}}}] ] }))) - .expect(1), - - // Mock an invalid token request - Mock::given(matchers::path("/.well-known/jmap")) - .and(matchers::method("GET")) - .and(matchers::header("Authorization", "Bearer MY_FAKE_TOKEN")) - .respond_with(ResponseTemplate::new(401)) - .expect(1), - - Mock::given(matchers::path("/jmap/api/")) - .and(matchers::method("POST")) - .and(matchers::header("Content-Type", "application/json")) - .and(matchers::header("Authorization", "Bearer MY_FAKE_TOKEN")) - .respond_with(ResponseTemplate::new(201)) - .expect(0), - ]) - .await; + .expect(1)).await; + + // Mock an invalid token request + server + .register( + Mock::given(matchers::path("/.well-known/jmap")) + .and(matchers::method("GET")) + .and(matchers::header("Authorization", "Bearer MY_FAKE_TOKEN")) + .respond_with(ResponseTemplate::new(401)) + .expect(1), + ) + .await; + + server + .register( + Mock::given(matchers::path("/jmap/api/")) + .and(matchers::method("POST")) + .and(matchers::header("Content-Type", "application/json")) + .and(matchers::header("Authorization", "Bearer MY_FAKE_TOKEN")) + .respond_with(ResponseTemplate::new(201)) + .expect(0), + ) + .await; let address = super::generate_with_api_url( &reqwest::Client::new(), @@ -179,9 +196,10 @@ mod tests { .await .unwrap_err(); - assert!(fake_token_error - .to_string() - .contains("Invalid Fastmail API token")); + assert_eq!( + fake_token_error.to_string(), + UsernameError::InvalidApiKey.to_string() + ); server.verify().await; } diff --git a/crates/bitwarden/src/tool/generators/username_forwarders/firefox.rs b/crates/bitwarden-generators/src/username_forwarders/firefox.rs similarity index 50% rename from crates/bitwarden/src/tool/generators/username_forwarders/firefox.rs rename to crates/bitwarden-generators/src/username_forwarders/firefox.rs index e53931358..66c2a3a2c 100644 --- a/crates/bitwarden/src/tool/generators/username_forwarders/firefox.rs +++ b/crates/bitwarden-generators/src/username_forwarders/firefox.rs @@ -3,13 +3,13 @@ use reqwest::{ StatusCode, }; -use crate::error::Result; +use crate::username::UsernameError; pub async fn generate( http: &reqwest::Client, api_token: String, website: Option, -) -> Result { +) -> Result { generate_with_api_url(http, api_token, website, "https://relay.firefox.com".into()).await } @@ -18,7 +18,7 @@ async fn generate_with_api_url( api_token: String, website: Option, api_url: String, -) -> Result { +) -> Result { #[derive(serde::Serialize)] struct Request { enabled: bool, @@ -41,7 +41,7 @@ async fn generate_with_api_url( .await?; if response.status() == StatusCode::UNAUTHORIZED { - return Err("Invalid Firefox Relay API key".into()); + return Err(UsernameError::InvalidApiKey); } // Throw any other errors @@ -60,24 +60,30 @@ async fn generate_with_api_url( mod tests { use serde_json::json; + use crate::username::UsernameError; + #[tokio::test] async fn test_mock_success() { use wiremock::{matchers, Mock, ResponseTemplate}; - let (server, _client) = - crate::util::start_mock(vec![Mock::given(matchers::path("/api/v1/relayaddresses/")) - .and(matchers::method("POST")) - .and(matchers::header("Content-Type", "application/json")) - .and(matchers::header("Authorization", "Token MY_TOKEN")) - .and(matchers::body_json(json!({ - "enabled": true, - "generated_for": "example.com", - "description": "example.com - Generated by Bitwarden." - }))) - .respond_with(ResponseTemplate::new(201).set_body_json(json!({ - "full_address": "ofuj4d4qw@mozmail.com" - }))) - .expect(1)]) + let server = wiremock::MockServer::start().await; + + server + .register( + Mock::given(matchers::path("/api/v1/relayaddresses/")) + .and(matchers::method("POST")) + .and(matchers::header("Content-Type", "application/json")) + .and(matchers::header("Authorization", "Token MY_TOKEN")) + .and(matchers::body_json(json!({ + "enabled": true, + "generated_for": "example.com", + "description": "example.com - Generated by Bitwarden." + }))) + .respond_with(ResponseTemplate::new(201).set_body_json(json!({ + "full_address": "ofuj4d4qw@mozmail.com" + }))) + .expect(1), + ) .await; let address = super::generate_with_api_url( @@ -97,19 +103,23 @@ mod tests { async fn test_mock_without_website() { use wiremock::{matchers, Mock, ResponseTemplate}; - let (server, _client) = - crate::util::start_mock(vec![Mock::given(matchers::path("/api/v1/relayaddresses/")) - .and(matchers::method("POST")) - .and(matchers::header("Content-Type", "application/json")) - .and(matchers::header("Authorization", "Token MY_OTHER_TOKEN")) - .and(matchers::body_json(json!({ - "enabled": true, - "description": "Generated by Bitwarden." - }))) - .respond_with(ResponseTemplate::new(201).set_body_json(json!({ - "full_address": "856f7765@mozmail.com" - }))) - .expect(1)]) + let server = wiremock::MockServer::start().await; + + server + .register( + Mock::given(matchers::path("/api/v1/relayaddresses/")) + .and(matchers::method("POST")) + .and(matchers::header("Content-Type", "application/json")) + .and(matchers::header("Authorization", "Token MY_OTHER_TOKEN")) + .and(matchers::body_json(json!({ + "enabled": true, + "description": "Generated by Bitwarden." + }))) + .respond_with(ResponseTemplate::new(201).set_body_json(json!({ + "full_address": "856f7765@mozmail.com" + }))) + .expect(1), + ) .await; let address = super::generate_with_api_url( @@ -129,18 +139,22 @@ mod tests { async fn test_mock_invalid_token() { use wiremock::{matchers, Mock, ResponseTemplate}; - let (server, _client) = - crate::util::start_mock(vec![Mock::given(matchers::path("/api/v1/relayaddresses/")) - .and(matchers::method("POST")) - .and(matchers::header("Content-Type", "application/json")) - .and(matchers::header("Authorization", "Token MY_FAKE_TOKEN")) - .and(matchers::body_json(json!({ - "enabled": true, - "generated_for": "example.com", - "description": "example.com - Generated by Bitwarden." - }))) - .respond_with(ResponseTemplate::new(401)) - .expect(1)]) + let server = wiremock::MockServer::start().await; + + server + .register( + Mock::given(matchers::path("/api/v1/relayaddresses/")) + .and(matchers::method("POST")) + .and(matchers::header("Content-Type", "application/json")) + .and(matchers::header("Authorization", "Token MY_FAKE_TOKEN")) + .and(matchers::body_json(json!({ + "enabled": true, + "generated_for": "example.com", + "description": "example.com - Generated by Bitwarden." + }))) + .respond_with(ResponseTemplate::new(401)) + .expect(1), + ) .await; let error = super::generate_with_api_url( @@ -152,7 +166,7 @@ mod tests { .await .unwrap_err(); - assert!(error.to_string().contains("Invalid Firefox Relay API key")); + assert_eq!(error.to_string(), UsernameError::InvalidApiKey.to_string()); server.verify().await; } diff --git a/crates/bitwarden-generators/src/username_forwarders/forwardemail.rs b/crates/bitwarden-generators/src/username_forwarders/forwardemail.rs new file mode 100644 index 000000000..1cec22882 --- /dev/null +++ b/crates/bitwarden-generators/src/username_forwarders/forwardemail.rs @@ -0,0 +1,209 @@ +use reqwest::{header::CONTENT_TYPE, StatusCode}; + +use crate::username::UsernameError; + +pub async fn generate( + http: &reqwest::Client, + api_token: String, + domain: String, + website: Option, +) -> Result { + generate_with_api_url( + http, + api_token, + domain, + website, + "https://api.forwardemail.net".into(), + ) + .await +} + +async fn generate_with_api_url( + http: &reqwest::Client, + api_token: String, + domain: String, + website: Option, + api_url: String, +) -> Result { + let description = super::format_description(&website); + + #[derive(serde::Serialize)] + struct Request { + labels: Option, + description: String, + } + + let response = http + .post(format!("{api_url}/v1/domains/{domain}/aliases")) + .header(CONTENT_TYPE, "application/json") + .basic_auth(api_token, None::) + .json(&Request { + description, + labels: website, + }) + .send() + .await?; + + if response.status() == StatusCode::UNAUTHORIZED { + return Err(UsernameError::InvalidApiKey); + } + + #[derive(serde::Deserialize)] + struct ResponseDomain { + name: Option, + } + #[derive(serde::Deserialize)] + struct Response { + name: Option, + domain: Option, + + message: Option, + error: Option, + } + let status = response.status(); + let response: Response = response.json().await?; + + if status.is_success() { + if let Some(name) = response.name { + if let Some(response_domain) = response.domain { + return Ok(format!( + "{}@{}", + name, + response_domain.name.unwrap_or(domain) + )); + } + } + } + + if let Some(message) = response.message { + return Err(UsernameError::ResponseContent { status, message }); + } + if let Some(message) = response.error { + return Err(UsernameError::ResponseContent { status, message }); + } + + Err(UsernameError::Unknown) +} + +#[cfg(test)] +mod tests { + use serde_json::json; + + use crate::username::UsernameError; + + #[tokio::test] + async fn test_mock_server() { + use wiremock::{matchers, Mock, ResponseTemplate}; + + let server = wiremock::MockServer::start().await; + + // Mock the request to the ForwardEmail API, and verify that the correct request is made + server + .register( + Mock::given(matchers::path("/v1/domains/mydomain.com/aliases")) + .and(matchers::method("POST")) + .and(matchers::header("Content-Type", "application/json")) + .and(matchers::header("Authorization", "Basic TVlfVE9LRU46")) + .and(matchers::body_json(json!({ + "labels": "example.com", + "description": "Website: example.com. Generated by Bitwarden." + }))) + .respond_with(ResponseTemplate::new(201).set_body_json(json!({ + "name": "wertg8ad", + "domain": { + "name": "mydomain.com" + } + }))) + .expect(1), + ) + .await; + + // Mock an invalid API token request + server + .register( + Mock::given(matchers::path("/v1/domains/mydomain.com/aliases")) + .and(matchers::method("POST")) + .and(matchers::header("Content-Type", "application/json")) + .and(matchers::header( + "Authorization", + "Basic TVlfRkFLRV9UT0tFTjo=", + )) + .and(matchers::body_json(json!({ + "labels": "example.com", + "description": "Website: example.com. Generated by Bitwarden." + }))) + .respond_with(ResponseTemplate::new(401).set_body_json(json!({ + "statusCode": 401, + "error": "Unauthorized", + "message": "Invalid API token." + }))) + .expect(1), + ) + .await; + + // Mock a free API token request + server + .register( + Mock::given(matchers::path("/v1/domains/mydomain.com/aliases")) + .and(matchers::method("POST")) + .and(matchers::header("Content-Type", "application/json")) + .and(matchers::header( + "Authorization", + "Basic TVlfRlJFRV9UT0tFTjo=", + )) + .and(matchers::body_json(json!({ + "labels": "example.com", + "description": "Website: example.com. Generated by Bitwarden." + }))) + .respond_with(ResponseTemplate::new(402).set_body_json(json!({ + "statusCode": 402, + "error": "Payment required", + "message": "Please upgrade to a paid plan to unlock this feature." + }))) + .expect(1), + ) + .await; + + let address = super::generate_with_api_url( + &reqwest::Client::new(), + "MY_TOKEN".into(), + "mydomain.com".into(), + Some("example.com".into()), + format!("http://{}", server.address()), + ) + .await + .unwrap(); + assert_eq!(address, "wertg8ad@mydomain.com"); + + let invalid_token_error = super::generate_with_api_url( + &reqwest::Client::new(), + "MY_FAKE_TOKEN".into(), + "mydomain.com".into(), + Some("example.com".into()), + format!("http://{}", server.address()), + ) + .await + .unwrap_err(); + + assert_eq!( + invalid_token_error.to_string(), + UsernameError::InvalidApiKey.to_string() + ); + + let free_token_error = super::generate_with_api_url( + &reqwest::Client::new(), + "MY_FREE_TOKEN".into(), + "mydomain.com".into(), + Some("example.com".into()), + format!("http://{}", server.address()), + ) + .await + .unwrap_err(); + + assert!(free_token_error + .to_string() + .contains("Please upgrade to a paid plan")); + + server.verify().await; + } +} diff --git a/crates/bitwarden/src/tool/generators/username_forwarders/mod.rs b/crates/bitwarden-generators/src/username_forwarders/mod.rs similarity index 100% rename from crates/bitwarden/src/tool/generators/username_forwarders/mod.rs rename to crates/bitwarden-generators/src/username_forwarders/mod.rs diff --git a/crates/bitwarden-generators/src/username_forwarders/simplelogin.rs b/crates/bitwarden-generators/src/username_forwarders/simplelogin.rs new file mode 100644 index 000000000..fa9342267 --- /dev/null +++ b/crates/bitwarden-generators/src/username_forwarders/simplelogin.rs @@ -0,0 +1,125 @@ +use reqwest::{header::CONTENT_TYPE, StatusCode}; + +use crate::username::UsernameError; + +pub async fn generate( + http: &reqwest::Client, + api_key: String, + website: Option, +) -> Result { + generate_with_api_url(http, api_key, website, "https://app.simplelogin.io".into()).await +} + +async fn generate_with_api_url( + http: &reqwest::Client, + api_key: String, + website: Option, + api_url: String, +) -> Result { + let query = website + .as_ref() + .map(|w| format!("?hostname={}", w)) + .unwrap_or_default(); + + let note = super::format_description(&website); + + #[derive(serde::Serialize)] + struct Request { + note: String, + } + + let response = http + .post(format!("{api_url}/api/alias/random/new{query}")) + .header(CONTENT_TYPE, "application/json") + .header("Authentication", api_key) + .json(&Request { note }) + .send() + .await?; + + if response.status() == StatusCode::UNAUTHORIZED { + return Err(UsernameError::InvalidApiKey); + } + + // Throw any other errors + response.error_for_status_ref()?; + + #[derive(serde::Deserialize)] + struct Response { + alias: String, + } + let response: Response = response.json().await?; + + Ok(response.alias) +} + +#[cfg(test)] +mod tests { + use serde_json::json; + + use crate::username::UsernameError; + #[tokio::test] + async fn test_mock_server() { + use wiremock::{matchers, Mock, ResponseTemplate}; + + let server = wiremock::MockServer::start().await; + + // Mock the request to the SimpleLogin API, and verify that the correct request is made + server + .register( + Mock::given(matchers::path("/api/alias/random/new")) + .and(matchers::method("POST")) + .and(matchers::query_param("hostname", "example.com")) + .and(matchers::header("Content-Type", "application/json")) + .and(matchers::header("Authentication", "MY_TOKEN")) + .and(matchers::body_json(json!({ + "note": "Website: example.com. Generated by Bitwarden." + }))) + .respond_with(ResponseTemplate::new(201).set_body_json(json!({ + "alias": "simplelogin.yut3g8@aleeas.com", + }))) + .expect(1), + ) + .await; + // Mock an invalid token request + server + .register( + Mock::given(matchers::path("/api/alias/random/new")) + .and(matchers::method("POST")) + .and(matchers::query_param("hostname", "example.com")) + .and(matchers::header("Content-Type", "application/json")) + .and(matchers::header("Authentication", "MY_FAKE_TOKEN")) + .and(matchers::body_json(json!({ + "note": "Website: example.com. Generated by Bitwarden." + }))) + .respond_with(ResponseTemplate::new(401)) + .expect(1), + ) + .await; + + let address = super::generate_with_api_url( + &reqwest::Client::new(), + "MY_TOKEN".into(), + Some("example.com".into()), + format!("http://{}", server.address()), + ) + .await + .unwrap(); + assert_eq!(address, "simplelogin.yut3g8@aleeas.com"); + + let fake_token_error = super::generate_with_api_url( + &reqwest::Client::new(), + "MY_FAKE_TOKEN".into(), + Some("example.com".into()), + format!("http://{}", server.address()), + ) + .await + .unwrap_err(); + + assert_eq!( + fake_token_error.to_string(), + UsernameError::InvalidApiKey.to_string() + ); + + server.verify().await; + } +} diff --git a/crates/bitwarden-generators/src/util.rs b/crates/bitwarden-generators/src/util.rs new file mode 100644 index 000000000..e434500ea --- /dev/null +++ b/crates/bitwarden-generators/src/util.rs @@ -0,0 +1,10 @@ +pub(crate) 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(), + } +} diff --git a/crates/bitwarden-generators/uniffi.toml b/crates/bitwarden-generators/uniffi.toml new file mode 100644 index 000000000..75f929b1a --- /dev/null +++ b/crates/bitwarden-generators/uniffi.toml @@ -0,0 +1,9 @@ +[bindings.kotlin] +package_name = "com.bitwarden.generators" +generate_immutable_records = true +android = true + +[bindings.swift] +ffi_module_name = "BitwardenGeneratorsFFI" +module_name = "BitwardenGenerators" +generate_immutable_records = true diff --git a/crates/bitwarden-json/src/command.rs b/crates/bitwarden-json/src/command.rs index 855a30399..8da5e8cbd 100644 --- a/crates/bitwarden-json/src/command.rs +++ b/crates/bitwarden-json/src/command.rs @@ -33,7 +33,6 @@ pub enum Command { /// This command is not capable of handling authentication requiring 2fa or captcha. /// /// Returns: [PasswordLoginResponse](bitwarden::auth::login::PasswordLoginResponse) - /// PasswordLogin(PasswordLoginRequest), #[cfg(feature = "internal")] @@ -42,7 +41,6 @@ pub enum Command { /// This command is for initiating an authentication handshake with Bitwarden. /// /// Returns: [ApiKeyLoginResponse](bitwarden::auth::login::ApiKeyLoginResponse) - /// ApiKeyLogin(ApiKeyLoginRequest), #[cfg(feature = "secrets")] @@ -51,7 +49,6 @@ pub enum Command { /// This command is for initiating an authentication handshake with Bitwarden. /// /// Returns: [ApiKeyLoginResponse](bitwarden::auth::login::ApiKeyLoginResponse) - /// AccessTokenLogin(AccessTokenLoginRequest), #[cfg(feature = "internal")] @@ -59,14 +56,12 @@ pub enum Command { /// Get the API key of the currently authenticated user /// /// Returns: [UserApiKeyResponse](bitwarden::platform::UserApiKeyResponse) - /// GetUserApiKey(SecretVerificationRequest), #[cfg(feature = "internal")] /// Get the user's passphrase /// /// Returns: String - /// Fingerprint(FingerprintRequest), #[cfg(feature = "internal")] @@ -74,7 +69,6 @@ pub enum Command { /// Retrieve all user data, ciphers and organizations the user is a part of /// /// Returns: [SyncResponse](bitwarden::platform::SyncResponse) - /// Sync(SyncRequest), #[cfg(feature = "secrets")] @@ -92,7 +86,6 @@ pub enum SecretsCommand { /// Retrieve a secret by the provided identifier /// /// Returns: [SecretResponse](bitwarden::secrets_manager::secrets::SecretResponse) - /// Get(SecretGetRequest), /// > Requires Authentication @@ -100,7 +93,6 @@ pub enum SecretsCommand { /// Retrieve secrets by the provided identifiers /// /// Returns: [SecretsResponse](bitwarden::secrets_manager::secrets::SecretsResponse) - /// GetByIds(SecretsGetRequest), /// > Requires Authentication @@ -108,15 +100,14 @@ pub enum SecretsCommand { /// Creates a new secret in the provided organization using the given data /// /// Returns: [SecretResponse](bitwarden::secrets_manager::secrets::SecretResponse) - /// Create(SecretCreateRequest), /// > Requires Authentication /// > Requires using an Access Token for login or calling Sync at least once - /// Lists all secret identifiers of the given organization, to then retrieve each secret, use `CreateSecret` + /// Lists all secret identifiers of the given organization, to then retrieve each secret, use + /// `CreateSecret` /// /// Returns: [SecretIdentifiersResponse](bitwarden::secrets_manager::secrets::SecretIdentifiersResponse) - /// List(SecretIdentifiersRequest), /// > Requires Authentication @@ -124,7 +115,6 @@ pub enum SecretsCommand { /// Updates an existing secret with the provided ID using the given data /// /// Returns: [SecretResponse](bitwarden::secrets_manager::secrets::SecretResponse) - /// Update(SecretPutRequest), /// > Requires Authentication @@ -132,7 +122,6 @@ pub enum SecretsCommand { /// Deletes all the secrets whose IDs match the provided ones /// /// Returns: [SecretsDeleteResponse](bitwarden::secrets_manager::secrets::SecretsDeleteResponse) - /// Delete(SecretsDeleteRequest), } @@ -145,7 +134,6 @@ pub enum ProjectsCommand { /// Retrieve a project by the provided identifier /// /// Returns: [ProjectResponse](bitwarden::secrets_manager::projects::ProjectResponse) - /// Get(ProjectGetRequest), /// > Requires Authentication @@ -153,7 +141,6 @@ pub enum ProjectsCommand { /// Creates a new project in the provided organization using the given data /// /// Returns: [ProjectResponse](bitwarden::secrets_manager::projects::ProjectResponse) - /// Create(ProjectCreateRequest), /// > Requires Authentication @@ -161,7 +148,6 @@ pub enum ProjectsCommand { /// Lists all projects of the given organization /// /// Returns: [ProjectsResponse](bitwarden::secrets_manager::projects::ProjectsResponse) - /// List(ProjectsListRequest), /// > Requires Authentication @@ -169,7 +155,6 @@ pub enum ProjectsCommand { /// Updates an existing project with the provided ID using the given data /// /// Returns: [ProjectResponse](bitwarden::secrets_manager::projects::ProjectResponse) - /// Update(ProjectPutRequest), /// > Requires Authentication @@ -177,6 +162,5 @@ pub enum ProjectsCommand { /// Deletes all the projects whose IDs match the provided ones /// /// Returns: [ProjectsDeleteResponse](bitwarden::secrets_manager::projects::ProjectsDeleteResponse) - /// Delete(ProjectsDeleteRequest), } diff --git a/crates/bitwarden-napi/package-lock.json b/crates/bitwarden-napi/package-lock.json index e10a23742..6d1288fa9 100644 --- a/crates/bitwarden-napi/package-lock.json +++ b/crates/bitwarden-napi/package-lock.json @@ -55,9 +55,9 @@ } }, "node_modules/@napi-rs/cli": { - "version": "2.17.0", - "resolved": "https://registry.npmjs.org/@napi-rs/cli/-/cli-2.17.0.tgz", - "integrity": "sha512-/M7MZ3dIqyFs6c0Bxtk+SOobPq6vYWYqBLYCOKx3dYWqoyJNBEgmDKUTrxIZu9eHw9Ill3WyEoHPqS9P99cd8A==", + "version": "2.18.0", + "resolved": "https://registry.npmjs.org/@napi-rs/cli/-/cli-2.18.0.tgz", + "integrity": "sha512-lfSRT7cs3iC4L+kv9suGYQEezn5Nii7Kpu+THsYVI0tA1Vh59LH45p4QADaD7hvIkmOz79eEGtoKQ9nAkAPkzA==", "dev": true, "bin": { "napi": "scripts/index.js" @@ -95,9 +95,9 @@ "dev": true }, "node_modules/@types/node": { - "version": "20.10.5", - "resolved": "https://registry.npmjs.org/@types/node/-/node-20.10.5.tgz", - "integrity": "sha512-nNPsNE65wjMxEKI93yOP+NPGGBJz/PoN3kZsVLee0XMiJolxSekEVD8wRwBUBqkwc7UWop0edW50yrCQW4CyRw==", + "version": "20.11.6", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.11.6.tgz", + "integrity": "sha512-+EOokTnksGVgip2PbYbr3xnR7kZigh4LbybAfBAw5BpnQ+FqBYUsvCEjYd70IXKlbohQ64mzEYmMtlWUY8q//Q==", "dev": true, "peer": true, "dependencies": { @@ -105,9 +105,9 @@ } }, "node_modules/acorn": { - "version": "8.11.2", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.11.2.tgz", - "integrity": "sha512-nc0Axzp/0FILLEVsm4fNwLCwMttvhEI263QtVPQcbpfZZ3ts0hLsZGOpE6czNlid7CJ9MlyH8reXkpsf3YUY4w==", + "version": "8.11.3", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.11.3.tgz", + "integrity": "sha512-Y9rRfJG5jcKOE0CLisYbojUjIrIEE7AGMzA/Sm4BslANhbS+cDMpgBdcPT91oJ7OuJ9hYJBx59RjbhxVnrF8Xg==", "dev": true, "bin": { "acorn": "bin/acorn" @@ -117,9 +117,9 @@ } }, "node_modules/acorn-walk": { - "version": "8.3.1", - "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.3.1.tgz", - "integrity": "sha512-TgUZgYvqZprrl7YldZNoa9OciCAyZR+Ejm9eXzKCmjsF5IKp/wgQ7Z/ZpjpGTIUPwrHQIcYeI8qDh4PsEwxMbw==", + "version": "8.3.2", + "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.3.2.tgz", + "integrity": "sha512-cjkyv4OtNCIeqhHrfS81QWXoCBPExR/J62oyEqepVw8WaQeSqpW2uhuLPh1m9eWhDuOo/jUXVTlifvesOWp/4A==", "dev": true, "engines": { "node": ">=0.4.0" diff --git a/crates/bitwarden-napi/src/client.rs b/crates/bitwarden-napi/src/client.rs index d794b18f4..712cd4ffd 100644 --- a/crates/bitwarden-napi/src/client.rs +++ b/crates/bitwarden-napi/src/client.rs @@ -29,7 +29,8 @@ pub struct BitwardenClient(JsonClient); impl BitwardenClient { #[napi(constructor)] pub fn new(settings_input: Option, log_level: Option) -> Self { - // This will only fail if another logger was already initialized, so we can ignore the result + // This will only fail if another logger was already initialized, so we can ignore the + // result let _ = env_logger::Builder::from_default_env() .filter_level(convert_level(log_level.unwrap_or(LogLevel::Info))) .try_init(); diff --git a/crates/bitwarden-napi/tsconfig.json b/crates/bitwarden-napi/tsconfig.json index f977e0759..8ec7e00d4 100644 --- a/crates/bitwarden-napi/tsconfig.json +++ b/crates/bitwarden-napi/tsconfig.json @@ -7,7 +7,7 @@ "strict": true, "noImplicitAny": true, "esModuleInterop": true, - "declaration": true + "declaration": true, }, - "include": ["src-ts", "src-ts/bitwarden_client", "src-ts/index.ts"] + "include": ["src-ts", "src-ts/bitwarden_client", "src-ts/index.ts"], } diff --git a/crates/bitwarden-uniffi/Cargo.toml b/crates/bitwarden-uniffi/Cargo.toml index 4bf7b085b..59db0daf7 100644 --- a/crates/bitwarden-uniffi/Cargo.toml +++ b/crates/bitwarden-uniffi/Cargo.toml @@ -12,16 +12,22 @@ crate-type = ["lib", "staticlib", "cdylib"] bench = false [dependencies] -async-lock = "3.2.0" +async-lock = "3.3.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.2" +uniffi = "=0.26.1" bitwarden = { path = "../bitwarden", features = ["mobile", "internal"] } +bitwarden-crypto = { path = "../bitwarden-crypto", version = "=0.1.0", features = [ + "mobile", +] } +bitwarden-generators = { path = "../bitwarden-generators", version = "=0.1.0", features = [ + "mobile", +] } [build-dependencies] -uniffi = { version = "=0.25.2", features = ["build"] } +uniffi = { version = "=0.26.1", features = ["build"] } diff --git a/crates/bitwarden-uniffi/src/auth/mod.rs b/crates/bitwarden-uniffi/src/auth/mod.rs index 1dc102b3e..62c791967 100644 --- a/crates/bitwarden-uniffi/src/auth/mod.rs +++ b/crates/bitwarden-uniffi/src/auth/mod.rs @@ -1,10 +1,9 @@ use std::sync::Arc; -use bitwarden::{ - auth::{password::MasterPasswordPolicyOptions, RegisterKeyResponse}, - client::kdf::Kdf, - crypto::HashPurpose, +use bitwarden::auth::{ + password::MasterPasswordPolicyOptions, AuthRequestResponse, RegisterKeyResponse, }; +use bitwarden_crypto::{AsymmetricEncString, HashPurpose, Kdf}; use crate::{error::Result, Client}; @@ -94,4 +93,20 @@ impl ClientAuth { .validate_password(password, password_hash.to_string()) .await?) } + + /// Initialize a new auth request + pub async fn new_auth_request(&self, email: String) -> Result { + Ok(self.0 .0.write().await.auth().new_auth_request(&email)?) + } + + /// Approve an auth request + pub async fn approve_auth_request(&self, public_key: String) -> Result { + Ok(self + .0 + .0 + .write() + .await + .auth() + .approve_auth_request(public_key)?) + } } diff --git a/crates/bitwarden-uniffi/src/crypto.rs b/crates/bitwarden-uniffi/src/crypto.rs index 25d577ba5..92afcbb87 100644 --- a/crates/bitwarden-uniffi/src/crypto.rs +++ b/crates/bitwarden-uniffi/src/crypto.rs @@ -3,6 +3,7 @@ use std::sync::Arc; use bitwarden::mobile::crypto::{ DerivePinKeyResponse, InitOrgCryptoRequest, InitUserCryptoRequest, }; +use bitwarden_crypto::EncString; use crate::{error::Result, Client}; @@ -11,7 +12,8 @@ pub struct ClientCrypto(pub(crate) Arc); #[uniffi::export(async_runtime = "tokio")] impl ClientCrypto { - /// Initialization method for the user crypto. Needs to be called before any other crypto operations. + /// Initialization method for the user crypto. Needs to be called before any other crypto + /// operations. pub async fn initialize_user_crypto(&self, req: InitUserCryptoRequest) -> Result<()> { Ok(self .0 @@ -23,7 +25,8 @@ impl ClientCrypto { .await?) } - /// Initialization method for the organization crypto. Needs to be called after `initialize_user_crypto` but before any other crypto operations. + /// Initialization method for the organization crypto. Needs to be called after + /// `initialize_user_crypto` but before any other crypto operations. pub async fn initialize_org_crypto(&self, req: InitOrgCryptoRequest) -> Result<()> { Ok(self .0 @@ -48,9 +51,23 @@ impl ClientCrypto { .await?) } - /// Generates a PIN protected user key from the provided PIN. The result can be stored and later used - /// to initialize another client instance by using the PIN and the PIN key with `initialize_user_crypto`. + /// Generates a PIN protected user key from the provided PIN. The result can be stored and later + /// used to initialize another client instance by using the PIN and the PIN key with + /// `initialize_user_crypto`. pub async fn derive_pin_key(&self, pin: String) -> Result { Ok(self.0 .0.write().await.crypto().derive_pin_key(pin).await?) } + + /// Derives the pin protected user key from encrypted pin. Used when pin requires master + /// password on first unlock. + pub async fn derive_pin_user_key(&self, encrypted_pin: EncString) -> Result { + Ok(self + .0 + .0 + .write() + .await + .crypto() + .derive_pin_user_key(encrypted_pin) + .await?) + } } diff --git a/crates/bitwarden-uniffi/src/docs.rs b/crates/bitwarden-uniffi/src/docs.rs index e7acbdcae..55d81beec 100644 --- a/crates/bitwarden-uniffi/src/docs.rs +++ b/crates/bitwarden-uniffi/src/docs.rs @@ -1,15 +1,15 @@ use bitwarden::{ auth::password::MasterPasswordPolicyOptions, - client::kdf::Kdf, - crypto::HashPurpose, + generators::{PassphraseGeneratorRequest, PasswordGeneratorRequest}, mobile::crypto::{InitOrgCryptoRequest, InitUserCryptoRequest}, platform::FingerprintRequest, - tool::{ExportFormat, PassphraseGeneratorRequest, PasswordGeneratorRequest}, + tool::ExportFormat, vault::{ Cipher, CipherView, Collection, Folder, FolderView, Send, SendListView, SendView, TotpResponse, }, }; +use bitwarden_crypto::{HashPurpose, Kdf}; use schemars::JsonSchema; #[derive(JsonSchema)] diff --git a/crates/bitwarden-uniffi/src/error.rs b/crates/bitwarden-uniffi/src/error.rs index b6175eb3b..5eef9bbd5 100644 --- a/crates/bitwarden-uniffi/src/error.rs +++ b/crates/bitwarden-uniffi/src/error.rs @@ -1,6 +1,7 @@ use std::fmt::{Display, Formatter}; -// Name is converted from *Error to *Exception, so we can't just name the enum Error because Exception already exists +// Name is converted from *Error to *Exception, so we can't just name the enum Error because +// Exception already exists #[derive(uniffi::Error, Debug)] #[uniffi(flat_error)] pub enum BitwardenError { diff --git a/crates/bitwarden-uniffi/src/tool/mod.rs b/crates/bitwarden-uniffi/src/tool/mod.rs index 6b443618c..4a4ea2401 100644 --- a/crates/bitwarden-uniffi/src/tool/mod.rs +++ b/crates/bitwarden-uniffi/src/tool/mod.rs @@ -1,10 +1,8 @@ use std::sync::Arc; use bitwarden::{ - tool::{ - ExportFormat, PassphraseGeneratorRequest, PasswordGeneratorRequest, - UsernameGeneratorRequest, - }, + generators::{PassphraseGeneratorRequest, PasswordGeneratorRequest, UsernameGeneratorRequest}, + tool::ExportFormat, vault::{Cipher, Collection, Folder}, }; diff --git a/crates/bitwarden-uniffi/src/uniffi_support.rs b/crates/bitwarden-uniffi/src/uniffi_support.rs index 72a67f698..663d5c41e 100644 --- a/crates/bitwarden-uniffi/src/uniffi_support.rs +++ b/crates/bitwarden-uniffi/src/uniffi_support.rs @@ -1,6 +1,7 @@ -use bitwarden::crypto::EncString; +use bitwarden_crypto::{AsymmetricEncString, EncString}; // Forward the type definitions to the main bitwarden crate type DateTime = chrono::DateTime; uniffi::ffi_converter_forward!(DateTime, bitwarden::UniFfiTag, crate::UniFfiTag); uniffi::ffi_converter_forward!(EncString, bitwarden::UniFfiTag, crate::UniFfiTag); +uniffi::ffi_converter_forward!(AsymmetricEncString, bitwarden::UniFfiTag, crate::UniFfiTag); diff --git a/crates/bitwarden-uniffi/src/vault/attachments.rs b/crates/bitwarden-uniffi/src/vault/attachments.rs new file mode 100644 index 000000000..0c099b779 --- /dev/null +++ b/crates/bitwarden-uniffi/src/vault/attachments.rs @@ -0,0 +1,94 @@ +use std::{path::Path, sync::Arc}; + +use bitwarden::vault::{Attachment, AttachmentEncryptResult, AttachmentView, Cipher}; + +use crate::{Client, Result}; + +#[derive(uniffi::Object)] +pub struct ClientAttachments(pub Arc); + +#[uniffi::export(async_runtime = "tokio")] +impl ClientAttachments { + /// Encrypt an attachment file in memory + pub async fn encrypt_buffer( + &self, + cipher: Cipher, + attachment: AttachmentView, + buffer: Vec, + ) -> Result { + Ok(self + .0 + .0 + .read() + .await + .vault() + .attachments() + .encrypt_buffer(cipher, attachment, &buffer) + .await?) + } + + /// Encrypt an attachment file located in the file system + pub async fn encrypt_file( + &self, + cipher: Cipher, + attachment: AttachmentView, + decrypted_file_path: String, + encrypted_file_path: String, + ) -> Result { + Ok(self + .0 + .0 + .read() + .await + .vault() + .attachments() + .encrypt_file( + cipher, + attachment, + Path::new(&decrypted_file_path), + Path::new(&encrypted_file_path), + ) + .await?) + } + /// Decrypt an attachment file in memory + pub async fn decrypt_buffer( + &self, + cipher: Cipher, + attachment: Attachment, + buffer: Vec, + ) -> Result> { + Ok(self + .0 + .0 + .read() + .await + .vault() + .attachments() + .decrypt_buffer(cipher, attachment, &buffer) + .await?) + } + + /// Decrypt an attachment file located in the file system + pub async fn decrypt_file( + &self, + cipher: Cipher, + attachment: Attachment, + encrypted_file_path: String, + decrypted_file_path: String, + ) -> Result<()> { + Ok(self + .0 + .0 + .read() + .await + .vault() + .attachments() + .decrypt_file( + cipher, + attachment, + Path::new(&encrypted_file_path), + Path::new(&decrypted_file_path), + ) + .await?) + } +} diff --git a/crates/bitwarden-uniffi/src/vault/mod.rs b/crates/bitwarden-uniffi/src/vault/mod.rs index 435e7f355..2205e0673 100644 --- a/crates/bitwarden-uniffi/src/vault/mod.rs +++ b/crates/bitwarden-uniffi/src/vault/mod.rs @@ -5,6 +5,7 @@ use chrono::{DateTime, Utc}; use crate::{error::Result, Client}; +pub mod attachments; pub mod ciphers; pub mod collections; pub mod folders; @@ -41,6 +42,11 @@ impl ClientVault { Arc::new(sends::ClientSends(self.0.clone())) } + /// Attachment file operations + pub fn attachments(self: Arc) -> Arc { + Arc::new(attachments::ClientAttachments(self.0.clone())) + } + /// Generate a TOTP code from a provided key. /// /// The key can be either: diff --git a/crates/bitwarden-uniffi/uniffi.toml b/crates/bitwarden-uniffi/uniffi.toml index c9cb0899c..8abfa33fc 100644 --- a/crates/bitwarden-uniffi/uniffi.toml +++ b/crates/bitwarden-uniffi/uniffi.toml @@ -2,6 +2,7 @@ package_name = "com.bitwarden.sdk" cdylib_name = "bitwarden_uniffi" generate_immutable_records = true +android = true [bindings.swift] ffi_module_name = "BitwardenFFI" diff --git a/crates/bitwarden-wasm/Cargo.toml b/crates/bitwarden-wasm/Cargo.toml index 4a4d7696c..9cbada205 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.66" +js-sys = "0.3.67" log = "0.4.20" serde = { version = "1.0.195", features = ["derive"] } -wasm-bindgen = { version = "0.2.89", features = ["serde-serialize"] } -wasm-bindgen-futures = "0.4.39" +wasm-bindgen = { version = "0.2.90", features = ["serde-serialize"] } +wasm-bindgen-futures = "0.4.40" bitwarden-json = { path = "../bitwarden-json", features = [ "secrets", @@ -22,4 +22,4 @@ bitwarden-json = { path = "../bitwarden-json", features = [ ] } [dev-dependencies] -wasm-bindgen-test = "0.3.39" +wasm-bindgen-test = "0.3.40" diff --git a/crates/bitwarden-wasm/src/client.rs b/crates/bitwarden-wasm/src/client.rs index 7d3d991db..b9f6723a6 100644 --- a/crates/bitwarden-wasm/src/client.rs +++ b/crates/bitwarden-wasm/src/client.rs @@ -26,8 +26,8 @@ fn convert_level(level: LogLevel) -> Level { } } -// Rc> is to avoid needing to take ownership of the Client during our async run_command function -// https://github.com/rustwasm/wasm-bindgen/issues/2195#issuecomment-799588401 +// Rc> is to avoid needing to take ownership of the Client during our async run_command +// function https://github.com/rustwasm/wasm-bindgen/issues/2195#issuecomment-799588401 #[wasm_bindgen] pub struct BitwardenClient(Rc>); diff --git a/crates/bitwarden/Cargo.toml b/crates/bitwarden/Cargo.toml index 79910bca4..1f666e21c 100644 --- a/crates/bitwarden/Cargo.toml +++ b/crates/bitwarden/Cargo.toml @@ -15,41 +15,36 @@ rust-version = "1.57" [features] default = ["secrets"] -secrets = [] # Secrets manager API -internal = [] # Internal testing methods -mobile = ["uniffi", "internal"] # Mobile-specific features +secrets = [] # Secrets manager API +internal = [] # Internal testing methods +mobile = [ + "uniffi", + "internal", + "bitwarden-crypto/mobile", + "bitwarden-generators/mobile", +] # Mobile-specific features wasm-bindgen = ["chrono/wasmbind"] [dependencies] -aes = ">=0.8.2, <0.9" -argon2 = { version = ">=0.5.0, <0.6", features = [ - "alloc", -], default-features = false } base64 = ">=0.21.2, <0.22" bitwarden-api-api = { path = "../bitwarden-api-api", version = "=0.2.3" } bitwarden-api-identity = { path = "../bitwarden-api-identity", version = "=0.2.3" } -cbc = { version = ">=0.1.2, <0.2", features = ["alloc"] } +bitwarden-crypto = { path = "../bitwarden-crypto", version = "=0.1.0" } +bitwarden-generators = { path = "../bitwarden-generators", version = "0.1.0" } 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" hmac = ">=0.12.1, <0.13" -lazy_static = ">=1.4.0, <2.0" log = ">=0.4.18, <0.5" -num-bigint = ">=0.4, <0.5" -num-traits = ">=0.2.15, <0.3" -pbkdf2 = { version = ">=0.12.1, <0.13", default-features = false } rand = ">=0.8.5, <0.9" reqwest = { version = ">=0.11, <0.12", features = [ "json", ], default-features = false } -rsa = ">=0.9.2, <0.10" schemars = { version = ">=0.8.9, <0.9", features = ["uuid1", "chrono"] } serde = { version = ">=1.0, <2.0", features = ["derive"] } serde_json = ">=1.0.96, <2.0" @@ -57,9 +52,8 @@ serde_qs = ">=0.12.0, <0.13" serde_repr = ">=0.1.12, <0.2" 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.2", optional = true, features = ["tokio"] } +uniffi = { version = "=0.26.1", optional = true, features = ["tokio"] } uuid = { version = ">=1.3.3, <2.0", features = ["serde"] } zxcvbn = ">= 2.2.2, <3.0" diff --git a/crates/bitwarden/src/admin_console/policy.rs b/crates/bitwarden/src/admin_console/policy.rs index 1b4acc310..9a4b0d16f 100644 --- a/crates/bitwarden/src/admin_console/policy.rs +++ b/crates/bitwarden/src/admin_console/policy.rs @@ -20,18 +20,20 @@ pub struct Policy { #[derive(Serialize_repr, Deserialize_repr, Debug, JsonSchema)] #[repr(u8)] pub enum PolicyType { - TwoFactorAuthentication = 0, // Requires users to have 2fa enabled - MasterPassword = 1, // Sets minimum requirements for master password complexity - PasswordGenerator = 2, // Sets minimum requirements/default type for generated passwords/passphrases + TwoFactorAuthentication = 0, // Requires users to have 2fa enabled + MasterPassword = 1, // Sets minimum requirements for master password complexity + PasswordGenerator = 2, /* Sets minimum requirements/default type for generated + * passwords/passphrases */ SingleOrg = 3, // Allows users to only be apart of one organization RequireSso = 4, // Requires users to authenticate with SSO PersonalOwnership = 5, // Disables personal vault ownership for adding/cloning items DisableSend = 6, // Disables the ability to create and edit Bitwarden Sends SendOptions = 7, // Sets restrictions or defaults for Bitwarden Sends - ResetPassword = 8, // Allows orgs to use reset password : also can enable auto-enrollment during invite flow - MaximumVaultTimeout = 9, // Sets the maximum allowed vault timeout + ResetPassword = 8, /* Allows orgs to use reset password : also can enable + * auto-enrollment during invite flow */ + MaximumVaultTimeout = 9, // Sets the maximum allowed vault timeout DisablePersonalVaultExport = 10, // Disable personal vault export - ActivateAutofill = 11, // Activates autofill with page load on the browser extension + ActivateAutofill = 11, // Activates autofill with page load on the browser extension } impl TryFrom for Policy { diff --git a/crates/bitwarden/src/auth/auth_request.rs b/crates/bitwarden/src/auth/auth_request.rs new file mode 100644 index 000000000..facdc8f82 --- /dev/null +++ b/crates/bitwarden/src/auth/auth_request.rs @@ -0,0 +1,137 @@ +use base64::{engine::general_purpose::STANDARD, Engine}; +use bitwarden_crypto::{ + fingerprint, AsymmetricCryptoKey, AsymmetricEncString, AsymmetricPublicCryptoKey, +}; +#[cfg(feature = "mobile")] +use bitwarden_crypto::{KeyDecryptable, SymmetricCryptoKey}; +use bitwarden_generators::{password, PasswordGeneratorRequest}; + +use crate::{error::Error, Client}; + +#[cfg_attr(feature = "mobile", derive(uniffi::Record))] +pub struct AuthRequestResponse { + /// Base64 encoded private key + /// This key is temporarily passed back and will most likely not be available in the future + pub private_key: String, + /// Base64 encoded public key + pub public_key: String, + /// Fingerprint of the public key + pub fingerprint: String, + /// Access code + pub access_code: String, +} + +/// Initiate a new auth request. +/// +/// Generates a private key and access code. The pulic key is uploaded to the server and transmitted +/// to another device. Where the user confirms the validity by confirming the fingerprint. The user +/// key is then encrypted using the public key and returned to the initiating device. +pub(crate) fn new_auth_request(email: &str) -> Result { + let mut rng = rand::thread_rng(); + + let key = AsymmetricCryptoKey::generate(&mut rng); + + let spki = key.to_public_der()?; + + let fingerprint = fingerprint(email, &spki)?; + let b64 = STANDARD.encode(&spki); + + Ok(AuthRequestResponse { + private_key: STANDARD.encode(key.to_der()?), + public_key: b64, + fingerprint, + access_code: password(PasswordGeneratorRequest { + length: 25, + lowercase: true, + uppercase: true, + numbers: true, + special: false, + ..Default::default() + })?, + }) +} + +/// Decrypt the user key using the private key generated previously. +#[cfg(feature = "mobile")] +pub(crate) fn auth_request_decrypt_user_key( + private_key: String, + user_key: AsymmetricEncString, +) -> Result { + let key = AsymmetricCryptoKey::from_der(&STANDARD.decode(private_key)?)?; + let key: String = user_key.decrypt_with_key(&key)?; + + Ok(key.parse()?) +} + +/// Approve an auth request. +/// +/// Encrypts the user key with a public key. +pub(crate) fn approve_auth_request( + client: &mut Client, + public_key: String, +) -> Result { + let public_key = AsymmetricPublicCryptoKey::from_der(&STANDARD.decode(public_key)?)?; + + let enc = client.get_encryption_settings()?; + let key = enc.get_key(&None).ok_or(Error::VaultLocked)?; + + Ok(AsymmetricEncString::encrypt_rsa2048_oaep_sha1( + &key.to_vec(), + &public_key, + )?) +} + +#[test] +fn test_auth_request() { + let request = new_auth_request("test@bitwarden.com").unwrap(); + + let secret = + "w2LO+nwV4oxwswVYCxlOfRUseXfvU03VzvKQHrqeklPgiMZrspUe6sOBToCnDn9Ay0tuCBn8ykVVRb7PWhub2Q=="; + + let private_key = + AsymmetricCryptoKey::from_der(&STANDARD.decode(&request.private_key).unwrap()).unwrap(); + + let encrypted = + AsymmetricEncString::encrypt_rsa2048_oaep_sha1(secret.as_bytes(), &private_key).unwrap(); + + let decrypted = auth_request_decrypt_user_key(request.private_key, encrypted).unwrap(); + + assert_eq!(decrypted.to_base64(), secret); +} + +#[cfg(test)] +mod tests { + use std::num::NonZeroU32; + + use bitwarden_crypto::Kdf; + + use super::*; + use crate::client::{LoginMethod, UserLoginMethod}; + + #[test] + fn test_approve() { + let mut client = Client::new(None); + client.set_login_method(LoginMethod::User(UserLoginMethod::Username { + client_id: "123".to_owned(), + email: "test@bitwarden.com".to_owned(), + kdf: Kdf::PBKDF2 { + iterations: NonZeroU32::new(600_000).unwrap(), + }, + })); + + let user_key = "2.Q/2PhzcC7GdeiMHhWguYAQ==|GpqzVdr0go0ug5cZh1n+uixeBC3oC90CIe0hd/HWA/pTRDZ8ane4fmsEIcuc8eMKUt55Y2q/fbNzsYu41YTZzzsJUSeqVjT8/iTQtgnNdpo=|dwI+uyvZ1h/iZ03VQ+/wrGEFYVewBUUl/syYgjsNMbE=".parse().unwrap(); + let private_key ="2.yN7l00BOlUE0Sb0M//Q53w==|EwKG/BduQRQ33Izqc/ogoBROIoI5dmgrxSo82sgzgAMIBt3A2FZ9vPRMY+GWT85JiqytDitGR3TqwnFUBhKUpRRAq4x7rA6A1arHrFp5Tp1p21O3SfjtvB3quiOKbqWk6ZaU1Np9HwqwAecddFcB0YyBEiRX3VwF2pgpAdiPbSMuvo2qIgyob0CUoC/h4Bz1be7Qa7B0Xw9/fMKkB1LpOm925lzqosyMQM62YpMGkjMsbZz0uPopu32fxzDWSPr+kekNNyLt9InGhTpxLmq1go/pXR2uw5dfpXc5yuta7DB0EGBwnQ8Vl5HPdDooqOTD9I1jE0mRyuBpWTTI3FRnu3JUh3rIyGBJhUmHqGZvw2CKdqHCIrQeQkkEYqOeJRJVdBjhv5KGJifqT3BFRwX/YFJIChAQpebNQKXe/0kPivWokHWwXlDB7S7mBZzhaAPidZvnuIhalE2qmTypDwHy22FyqV58T8MGGMchcASDi/QXI6kcdpJzPXSeU9o+NC68QDlOIrMVxKFeE7w7PvVmAaxEo0YwmuAzzKy9QpdlK0aab/xEi8V4iXj4hGepqAvHkXIQd+r3FNeiLfllkb61p6WTjr5urcmDQMR94/wYoilpG5OlybHdbhsYHvIzYoLrC7fzl630gcO6t4nM24vdB6Ymg9BVpEgKRAxSbE62Tqacxqnz9AcmgItb48NiR/He3n3ydGjPYuKk/ihZMgEwAEZvSlNxYONSbYrIGDtOY+8Nbt6KiH3l06wjZW8tcmFeVlWv+tWotnTY9IqlAfvNVTjtsobqtQnvsiDjdEVtNy/s2ci5TH+NdZluca2OVEr91Wayxh70kpM6ib4UGbfdmGgCo74gtKvKSJU0rTHakQ5L9JlaSDD5FamBRyI0qfL43Ad9qOUZ8DaffDCyuaVyuqk7cz9HwmEmvWU3VQ+5t06n/5kRDXttcw8w+3qClEEdGo1KeENcnXCB32dQe3tDTFpuAIMLqwXs6FhpawfZ5kPYvLPczGWaqftIs/RXJ/EltGc0ugw2dmTLpoQhCqrcKEBDoYVk0LDZKsnzitOGdi9mOWse7Se8798ib1UsHFUjGzISEt6upestxOeupSTOh0v4+AjXbDzRUyogHww3V+Bqg71bkcMxtB+WM+pn1XNbVTyl9NR040nhP7KEf6e9ruXAtmrBC2ah5cFEpLIot77VFZ9ilLuitSz+7T8n1yAh1IEG6xxXxninAZIzi2qGbH69O5RSpOJuJTv17zTLJQIIc781JwQ2TTwTGnx5wZLbffhCasowJKd2EVcyMJyhz6ru0PvXWJ4hUdkARJs3Xu8dus9a86N8Xk6aAPzBDqzYb1vyFIfBxP0oO8xFHgd30Cgmz8UrSE3qeWRrF8ftrI6xQnFjHBGWD/JWSvd6YMcQED0aVuQkuNW9ST/DzQThPzRfPUoiL10yAmV7Ytu4fR3x2sF0Yfi87YhHFuCMpV/DsqxmUizyiJuD938eRcH8hzR/VO53Qo3UIsqOLcyXtTv6THjSlTopQ+JOLOnHm1w8dzYbLN44OG44rRsbihMUQp+wUZ6bsI8rrOnm9WErzkbQFbrfAINdoCiNa6cimYIjvvnMTaFWNymqY1vZxGztQiMiHiHYwTfwHTXrb9j0uPM=|09J28iXv9oWzYtzK2LBT6Yht4IT4MijEkk0fwFdrVQ4=".parse().unwrap(); + client + .initialize_user_crypto("asdfasdfasdf", user_key, private_key) + .unwrap(); + + let public_key = "MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAnRtpYLp9QLaEUkdPkWZX6TrMUKFoSaFamBKDL0NlS6xwtETTqYIxRVsvnHii3Dhz+fh3aHQVyBa1rBXogeH3MLERzNADwZhpWtBT9wKCXY5o0fIWYdZV/Nf0Y+0ZoKdImrGPLPmyHGfCqrvrK7g09q8+3kXUlkdAImlQqc5TiYwiHBfUQVTBq/Ae7a0FEpajx1NUM4h3edpCYxbvnpSTuzMgbmbUUS4gdCaheA2ibYxy/zkLzsaLygoibMyGNl9Y8J5n7dDrVXpUKZTihVfXwHfEZwtKNunWsmmt8rEJWVpguUDEDVSUogoxQcNaCi7KHn9ioSip76hg1jLpypO3WwIDAQAB"; + + // Verify fingerprint + let pbkey = STANDARD.decode(public_key).unwrap(); + let fingerprint = fingerprint("test@bitwarden.com", &pbkey).unwrap(); + assert_eq!(fingerprint, "spill-applaud-sweep-habitable-shrunk"); + + approve_auth_request(&mut client, public_key.to_owned()).unwrap(); + } +} diff --git a/crates/bitwarden/src/auth/client_auth.rs b/crates/bitwarden/src/auth/client_auth.rs index 6c9ddbd4d..aaa387741 100644 --- a/crates/bitwarden/src/auth/client_auth.rs +++ b/crates/bitwarden/src/auth/client_auth.rs @@ -1,9 +1,13 @@ +#[cfg(feature = "internal")] +use bitwarden_crypto::{AsymmetricEncString, DeviceKey, TrustDeviceResponse}; + #[cfg(feature = "secrets")] use crate::auth::login::{login_access_token, AccessTokenLoginRequest, AccessTokenLoginResponse}; use crate::{auth::renew::renew_token, error::Result, Client}; #[cfg(feature = "internal")] use crate::{ auth::{ + auth_request::{approve_auth_request, new_auth_request}, login::{ login_api_key, login_password, send_two_factor_email, ApiKeyLoginRequest, ApiKeyLoginResponse, PasswordLoginRequest, PasswordLoginResponse, @@ -13,9 +17,10 @@ use crate::{ password_strength, satisfies_policy, validate_password, MasterPasswordPolicyOptions, }, register::{make_register_keys, register}, - RegisterKeyResponse, RegisterRequest, + AuthRequestResponse, RegisterKeyResponse, RegisterRequest, }, - client::kdf::Kdf, + client::Kdf, + error::Error, }; pub struct ClientAuth<'a> { @@ -70,9 +75,10 @@ impl<'a> ClientAuth<'a> { } pub async fn prelogin(&mut self, email: String) -> Result { - use crate::auth::login::request_prelogin; + use crate::auth::login::{parse_prelogin, request_prelogin}; - request_prelogin(self.client, email).await?.try_into() + let response = request_prelogin(self.client, email).await?; + parse_prelogin(response) } pub async fn login_password( @@ -96,6 +102,27 @@ impl<'a> ClientAuth<'a> { pub async fn validate_password(&self, password: String, password_hash: String) -> Result { validate_password(self.client, password, password_hash).await } + + pub fn new_auth_request(&self, email: &str) -> Result { + new_auth_request(email) + } + + pub fn approve_auth_request(&mut self, public_key: String) -> Result { + approve_auth_request(self.client, public_key) + } + + pub async fn trust_device(&self) -> Result { + trust_device(self.client) + } +} + +#[cfg(feature = "internal")] +fn trust_device(client: &Client) -> Result { + let enc = client.get_encryption_settings()?; + + let user_key = enc.get_key(&None).ok_or(Error::VaultLocked)?; + + Ok(DeviceKey::trust_device(user_key)?) } impl<'a> Client { @@ -171,7 +198,7 @@ mod tests { .login_access_token(&AccessTokenLoginRequest { access_token: "0.ec2c1d46-6a4b-4751-a310-af9601317f2d.C2IgxjjLF7qSshsbwe8JGcbM075YXw:X8vbvA0bduihIDe/qrzIQQ==".into(), state_file: None, - },) + }) .await .unwrap(); assert!(res.authenticated); diff --git a/crates/bitwarden/src/auth/login/access_token.rs b/crates/bitwarden/src/auth/login/access_token.rs index 43418d5f1..d540b31a8 100644 --- a/crates/bitwarden/src/auth/login/access_token.rs +++ b/crates/bitwarden/src/auth/login/access_token.rs @@ -1,6 +1,7 @@ use std::path::{Path, PathBuf}; use base64::{engine::general_purpose::STANDARD, Engine}; +use bitwarden_crypto::{EncString, KeyDecryptable, SymmetricCryptoKey}; use chrono::Utc; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; @@ -13,7 +14,6 @@ use crate::{ JWTToken, }, client::{AccessToken, LoginMethod, ServiceAccountLoginMethod}, - crypto::{EncString, KeyDecryptable, SymmetricCryptoKey}, error::{Error, Result}, secrets_manager::state::{self, ClientState}, Client, @@ -63,8 +63,8 @@ pub(crate) async fn login_access_token( } let payload: Payload = serde_json::from_slice(&decrypted_payload)?; - let encryption_key = STANDARD.decode(payload.encryption_key.clone())?; - let encryption_key = SymmetricCryptoKey::try_from(encryption_key.as_slice())?; + let mut encryption_key = STANDARD.decode(payload.encryption_key.clone())?; + let encryption_key = SymmetricCryptoKey::try_from(encryption_key.as_mut_slice())?; let access_token_obj: JWTToken = r.access_token.parse()?; diff --git a/crates/bitwarden/src/auth/login/api_key.rs b/crates/bitwarden/src/auth/login/api_key.rs index 9370559b4..e161ececd 100644 --- a/crates/bitwarden/src/auth/login/api_key.rs +++ b/crates/bitwarden/src/auth/login/api_key.rs @@ -1,3 +1,4 @@ +use bitwarden_crypto::EncString; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; @@ -8,7 +9,6 @@ use crate::{ JWTToken, }, client::{LoginMethod, UserLoginMethod}, - crypto::EncString, error::Result, Client, }; diff --git a/crates/bitwarden/src/auth/login/mod.rs b/crates/bitwarden/src/auth/login/mod.rs index d2f66b569..afd7873a4 100644 --- a/crates/bitwarden/src/auth/login/mod.rs +++ b/crates/bitwarden/src/auth/login/mod.rs @@ -1,6 +1,6 @@ #[cfg(feature = "internal")] use { - crate::{client::Client, error::Result}, + crate::{client::Kdf, error::Result, Client}, bitwarden_api_identity::{ apis::accounts_api::accounts_prelogin_post, models::{PreloginRequestModel, PreloginResponseModel}, @@ -45,3 +45,40 @@ pub(crate) async fn request_prelogin( let config = client.get_api_configurations().await; Ok(accounts_prelogin_post(&config.identity, Some(request_model)).await?) } + +#[cfg(feature = "internal")] +pub(crate) fn parse_prelogin(response: PreloginResponseModel) -> Result { + use std::num::NonZeroU32; + + use bitwarden_api_identity::models::KdfType; + + use crate::util::{ + default_argon2_iterations, default_argon2_memory, default_argon2_parallelism, + default_pbkdf2_iterations, + }; + + let kdf = response.kdf.ok_or("KDF not found")?; + + Ok(match kdf { + KdfType::Variant0 => Kdf::PBKDF2 { + iterations: response + .kdf_iterations + .and_then(|e| NonZeroU32::new(e as u32)) + .unwrap_or_else(default_pbkdf2_iterations), + }, + KdfType::Variant1 => Kdf::Argon2id { + iterations: response + .kdf_iterations + .and_then(|e| NonZeroU32::new(e as u32)) + .unwrap_or_else(default_argon2_iterations), + memory: response + .kdf_memory + .and_then(|e| NonZeroU32::new(e as u32)) + .unwrap_or_else(default_argon2_memory), + parallelism: response + .kdf_parallelism + .and_then(|e| NonZeroU32::new(e as u32)) + .unwrap_or_else(default_argon2_parallelism), + }, + }) +} diff --git a/crates/bitwarden/src/auth/login/password.rs b/crates/bitwarden/src/auth/login/password.rs index 7a93c68e0..f873ace97 100644 --- a/crates/bitwarden/src/auth/login/password.rs +++ b/crates/bitwarden/src/auth/login/password.rs @@ -6,8 +6,7 @@ use serde::{Deserialize, Serialize}; #[cfg(feature = "internal")] use crate::{ auth::{api::request::PasswordTokenRequest, login::TwoFactorRequest}, - client::{kdf::Kdf, LoginMethod}, - crypto::EncString, + client::{Kdf, LoginMethod}, Client, }; use crate::{ @@ -23,7 +22,9 @@ pub(crate) async fn login_password( client: &mut Client, input: &PasswordLoginRequest, ) -> Result { - use crate::{auth::determine_password_hash, client::UserLoginMethod, crypto::HashPurpose}; + use bitwarden_crypto::{EncString, HashPurpose}; + + use crate::{auth::determine_password_hash, client::UserLoginMethod}; info!("password logging in"); debug!("{:#?}, {:#?}", client, input); @@ -93,9 +94,11 @@ pub struct PasswordLoginResponse { pub reset_master_password: bool, /// Whether or not the user is required to update their master password pub force_password_reset: bool, - /// The available two factor authentication options. Present only when authentication fails due to requiring a second authentication factor. + /// The available two factor authentication options. Present only when authentication fails due + /// to requiring a second authentication factor. pub two_factor: Option, - /// The information required to present the user with a captcha challenge. Only present when authentication fails due to requiring validation of a captcha challenge. + /// The information required to present the user with a captcha challenge. Only present when + /// authentication fails due to requiring validation of a captcha challenge. pub captcha: Option, } diff --git a/crates/bitwarden/src/auth/login/two_factor.rs b/crates/bitwarden/src/auth/login/two_factor.rs index cf3cb7907..45be042c7 100644 --- a/crates/bitwarden/src/auth/login/two_factor.rs +++ b/crates/bitwarden/src/auth/login/two_factor.rs @@ -1,9 +1,10 @@ use bitwarden_api_api::models::TwoFactorEmailRequestModel; +use bitwarden_crypto::HashPurpose; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; use serde_repr::{Deserialize_repr, Serialize_repr}; -use crate::{auth::determine_password_hash, crypto::HashPurpose, error::Result, Client}; +use crate::{auth::determine_password_hash, error::Result, Client}; #[derive(Serialize, Deserialize, Debug, JsonSchema)] #[serde(rename_all = "camelCase", deny_unknown_fields)] diff --git a/crates/bitwarden/src/auth/mod.rs b/crates/bitwarden/src/auth/mod.rs index 49be82d1c..23b64eaf9 100644 --- a/crates/bitwarden/src/auth/mod.rs +++ b/crates/bitwarden/src/auth/mod.rs @@ -6,18 +6,21 @@ pub mod login; pub mod password; pub mod renew; pub use jwt_token::JWTToken; - #[cfg(feature = "internal")] mod register; #[cfg(feature = "internal")] +use bitwarden_crypto::{HashPurpose, MasterKey}; +#[cfg(feature = "internal")] pub use register::{RegisterKeyResponse, RegisterRequest}; +#[cfg(feature = "internal")] +mod auth_request; +#[cfg(feature = "mobile")] +pub(crate) use auth_request::auth_request_decrypt_user_key; +#[cfg(feature = "internal")] +pub use auth_request::AuthRequestResponse; #[cfg(feature = "internal")] -use crate::{ - client::kdf::Kdf, - crypto::{HashPurpose, MasterKey}, - error::Result, -}; +use crate::{client::Kdf, error::Result}; #[cfg(feature = "internal")] async fn determine_password_hash( @@ -27,7 +30,7 @@ async fn determine_password_hash( purpose: HashPurpose, ) -> Result { let master_key = MasterKey::derive(password.as_bytes(), email.as_bytes(), kdf)?; - master_key.derive_master_key_hash(password.as_bytes(), purpose) + Ok(master_key.derive_master_key_hash(password.as_bytes(), purpose)?) } #[cfg(test)] diff --git a/crates/bitwarden/src/auth/password/validate.rs b/crates/bitwarden/src/auth/password/validate.rs index c86c1c40d..f6d22e11a 100644 --- a/crates/bitwarden/src/auth/password/validate.rs +++ b/crates/bitwarden/src/auth/password/validate.rs @@ -1,7 +1,8 @@ +use bitwarden_crypto::HashPurpose; + use crate::{ auth::determine_password_hash, client::{LoginMethod, UserLoginMethod}, - crypto::HashPurpose, error::{Error, Result}, Client, }; @@ -40,7 +41,7 @@ mod tests { use std::num::NonZeroU32; use super::validate_password; - use crate::client::{kdf::Kdf, Client, LoginMethod, UserLoginMethod}; + use crate::client::{Client, Kdf, LoginMethod, UserLoginMethod}; let mut client = Client::new(None); client.set_login_method(LoginMethod::User(UserLoginMethod::Username { diff --git a/crates/bitwarden/src/auth/register.rs b/crates/bitwarden/src/auth/register.rs index 206c1905b..2b0c9503b 100644 --- a/crates/bitwarden/src/auth/register.rs +++ b/crates/bitwarden/src/auth/register.rs @@ -2,16 +2,11 @@ use bitwarden_api_identity::{ apis::accounts_api::accounts_register_post, models::{KeysRequestModel, RegisterRequestModel}, }; +use bitwarden_crypto::{HashPurpose, MasterKey, RsaKeyPair}; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; -use crate::{ - client::kdf::Kdf, - crypto::{HashPurpose, MasterKey, RsaKeyPair}, - error::Result, - util::default_pbkdf2_iterations, - Client, -}; +use crate::{client::Kdf, error::Result, util::default_pbkdf2_iterations, Client}; #[derive(Serialize, Deserialize, Debug, JsonSchema)] #[serde(rename_all = "camelCase", deny_unknown_fields)] diff --git a/crates/bitwarden/src/client/access_token.rs b/crates/bitwarden/src/client/access_token.rs index 854922c14..d7cad2fca 100644 --- a/crates/bitwarden/src/client/access_token.rs +++ b/crates/bitwarden/src/client/access_token.rs @@ -1,13 +1,10 @@ use std::{fmt::Debug, str::FromStr}; use base64::Engine; +use bitwarden_crypto::{derive_shareable_key, SymmetricCryptoKey}; use uuid::Uuid; -use crate::{ - crypto::{derive_shareable_key, SymmetricCryptoKey}, - error::AccessTokenInvalidError, - util::STANDARD_INDIFFERENT, -}; +use crate::{error::AccessTokenInvalidError, util::STANDARD_INDIFFERENT}; pub struct AccessToken { pub access_token_id: Uuid, @@ -91,7 +88,8 @@ mod tests { use crate::client::AccessToken; - // Encryption key without base64 padding, we generate it with padding but ignore it when decoding + // Encryption key without base64 padding, we generate it with padding but ignore it when + // decoding let t = "0.ec2c1d46-6a4b-4751-a310-af9601317f2d.C2IgxjjLF7qSshsbwe8JGcbM075YXw:X8vbvA0bduihIDe/qrzIQQ"; assert!(AccessToken::from_str(t).is_ok()); diff --git a/crates/bitwarden/src/client/client.rs b/crates/bitwarden/src/client/client.rs index 2c5d9b387..3ccb7f9ca 100644 --- a/crates/bitwarden/src/client/client.rs +++ b/crates/bitwarden/src/client/client.rs @@ -1,5 +1,10 @@ use std::path::PathBuf; +#[cfg(feature = "internal")] +pub use bitwarden_crypto::Kdf; +use bitwarden_crypto::SymmetricCryptoKey; +#[cfg(feature = "internal")] +use bitwarden_crypto::{AsymmetricEncString, EncString}; use chrono::Utc; use reqwest::header::{self}; use uuid::Uuid; @@ -8,20 +13,15 @@ use super::AccessToken; #[cfg(feature = "secrets")] use crate::auth::login::{AccessTokenLoginRequest, AccessTokenLoginResponse}; #[cfg(feature = "internal")] -use crate::{ - client::kdf::Kdf, - crypto::{AsymmEncString, EncString}, - platform::{ - get_user_api_key, sync, SecretVerificationRequest, SyncRequest, SyncResponse, - UserApiKeyResponse, - }, +use crate::platform::{ + get_user_api_key, sync, SecretVerificationRequest, SyncRequest, SyncResponse, + UserApiKeyResponse, }; use crate::{ client::{ client_settings::{ClientSettings, DeviceType}, encryption_settings::EncryptionSettings, }, - crypto::SymmetricCryptoKey, error::{Error, Result}, }; @@ -255,7 +255,7 @@ impl Client { pin_protected_user_key: EncString, private_key: EncString, ) -> Result<&EncryptionSettings> { - use crate::crypto::MasterKey; + use bitwarden_crypto::MasterKey; let pin_key = match &self.login_method { Some(LoginMethod::User( @@ -280,7 +280,7 @@ impl Client { #[cfg(feature = "internal")] pub(crate) fn initialize_org_crypto( &mut self, - org_keys: Vec<(Uuid, AsymmEncString)>, + org_keys: Vec<(Uuid, AsymmetricEncString)>, ) -> Result<&EncryptionSettings> { let enc = self .encryption_settings diff --git a/crates/bitwarden/src/client/client_settings.rs b/crates/bitwarden/src/client/client_settings.rs index 172baf733..2f0637b4a 100644 --- a/crates/bitwarden/src/client/client_settings.rs +++ b/crates/bitwarden/src/client/client_settings.rs @@ -1,8 +1,8 @@ use schemars::JsonSchema; use serde::{Deserialize, Serialize}; -/// 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. +/// 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. /// /// Defaults to /// diff --git a/crates/bitwarden/src/client/encryption_settings.rs b/crates/bitwarden/src/client/encryption_settings.rs index ebfbba2b4..6e4da9895 100644 --- a/crates/bitwarden/src/client/encryption_settings.rs +++ b/crates/bitwarden/src/client/encryption_settings.rs @@ -1,22 +1,16 @@ use std::collections::HashMap; -use rsa::RsaPrivateKey; -use uuid::Uuid; +use bitwarden_crypto::{AsymmetricCryptoKey, KeyContainer, SymmetricCryptoKey}; #[cfg(feature = "internal")] -use { - crate::{ - client::UserLoginMethod, - crypto::{AsymmEncString, EncString, KeyDecryptable}, - error::{CryptoError, Result}, - }, - rsa::pkcs8::DecodePrivateKey, -}; +use bitwarden_crypto::{AsymmetricEncString, EncString}; +use uuid::Uuid; -use crate::crypto::SymmetricCryptoKey; +#[cfg(feature = "internal")] +use crate::{client::UserLoginMethod, error::Result}; pub struct EncryptionSettings { user_key: SymmetricCryptoKey, - pub(crate) private_key: Option, + pub(crate) private_key: Option, org_keys: HashMap, } @@ -35,7 +29,7 @@ impl EncryptionSettings { user_key: EncString, private_key: EncString, ) -> Result { - use crate::crypto::MasterKey; + use bitwarden_crypto::MasterKey; match login_method { UserLoginMethod::Username { email, kdf, .. } @@ -51,17 +45,20 @@ impl EncryptionSettings { } } - /// Initialize the encryption settings with the decrypted user key and the encrypted user private key - /// This should only be used when unlocking the vault via biometrics or when the vault is set to lock: "never" - /// Otherwise handling the decrypted user key is dangerous and discouraged + /// Initialize the encryption settings with the decrypted user key and the encrypted user + /// private key This should only be used when unlocking the vault via biometrics or when the + /// vault is set to lock: "never" Otherwise handling the decrypted user key is dangerous and + /// discouraged #[cfg(feature = "internal")] pub(crate) fn new_decrypted_key( user_key: SymmetricCryptoKey, private_key: EncString, ) -> Result { + use bitwarden_crypto::KeyDecryptable; + let private_key = { let dec: Vec = private_key.decrypt_with_key(&user_key)?; - Some(rsa::RsaPrivateKey::from_pkcs8_der(&dec).map_err(|_| CryptoError::InvalidKey)?) + Some(AsymmetricCryptoKey::from_der(&dec)?) }; Ok(EncryptionSettings { @@ -84,8 +81,10 @@ impl EncryptionSettings { #[cfg(feature = "internal")] pub(crate) fn set_org_keys( &mut self, - org_enc_keys: Vec<(Uuid, AsymmEncString)>, + org_enc_keys: Vec<(Uuid, AsymmetricEncString)>, ) -> Result<&mut Self> { + use bitwarden_crypto::KeyDecryptable; + use crate::error::Error; let private_key = self.private_key.as_ref().ok_or(Error::VaultLocked)?; @@ -96,9 +95,9 @@ impl EncryptionSettings { // Decrypt the org keys with the private key for (org_id, org_enc_key) in org_enc_keys { - let dec = org_enc_key.decrypt(private_key)?; + let mut dec: Vec = org_enc_key.decrypt_with_key(private_key)?; - let org_key = SymmetricCryptoKey::try_from(dec.as_slice())?; + let org_key = SymmetricCryptoKey::try_from(dec.as_mut_slice())?; self.org_keys.insert(org_id, org_key); } @@ -107,7 +106,8 @@ impl EncryptionSettings { } pub(crate) fn get_key(&self, org_id: &Option) -> Option<&SymmetricCryptoKey> { - // If we don't have a private key set (to decode multiple org keys), we just use the main user key + // If we don't have a private key set (to decode multiple org keys), we just use the main + // user key if self.private_key.is_none() { return Some(&self.user_key); } @@ -118,3 +118,9 @@ impl EncryptionSettings { } } } + +impl KeyContainer for EncryptionSettings { + fn get_key(&self, org_id: &Option) -> Option<&SymmetricCryptoKey> { + EncryptionSettings::get_key(self, org_id) + } +} diff --git a/crates/bitwarden/src/client/kdf.rs b/crates/bitwarden/src/client/kdf.rs deleted file mode 100644 index 2f6787d8d..000000000 --- a/crates/bitwarden/src/client/kdf.rs +++ /dev/null @@ -1,60 +0,0 @@ -use std::num::NonZeroU32; - -#[cfg(feature = "internal")] -use bitwarden_api_identity::models::{KdfType, PreloginResponseModel}; -use schemars::JsonSchema; -use serde::{Deserialize, Serialize}; - -#[cfg(feature = "internal")] -use crate::error::{Error, Result}; - -#[derive(Serialize, Deserialize, Debug, JsonSchema, Clone)] -#[serde(rename_all = "camelCase", deny_unknown_fields)] -#[cfg_attr(feature = "mobile", derive(uniffi::Enum))] -pub enum Kdf { - PBKDF2 { - iterations: NonZeroU32, - }, - Argon2id { - iterations: NonZeroU32, - memory: NonZeroU32, - parallelism: NonZeroU32, - }, -} - -#[cfg(feature = "internal")] -impl TryFrom for Kdf { - type Error = Error; - - fn try_from(response: PreloginResponseModel) -> Result { - use crate::util::{ - default_argon2_iterations, default_argon2_memory, default_argon2_parallelism, - default_pbkdf2_iterations, - }; - - let kdf = response.kdf.ok_or("KDF not found")?; - - Ok(match kdf { - KdfType::Variant0 => Kdf::PBKDF2 { - iterations: response - .kdf_iterations - .and_then(|e| NonZeroU32::new(e as u32)) - .unwrap_or_else(default_pbkdf2_iterations), - }, - KdfType::Variant1 => Kdf::Argon2id { - iterations: response - .kdf_iterations - .and_then(|e| NonZeroU32::new(e as u32)) - .unwrap_or_else(default_argon2_iterations), - memory: response - .kdf_memory - .and_then(|e| NonZeroU32::new(e as u32)) - .unwrap_or_else(default_argon2_memory), - parallelism: response - .kdf_parallelism - .and_then(|e| NonZeroU32::new(e as u32)) - .unwrap_or_else(default_argon2_parallelism), - }, - }) - } -} diff --git a/crates/bitwarden/src/client/mod.rs b/crates/bitwarden/src/client/mod.rs index 25a2f5db0..c3719fce2 100644 --- a/crates/bitwarden/src/client/mod.rs +++ b/crates/bitwarden/src/client/mod.rs @@ -6,7 +6,6 @@ pub(crate) mod access_token; mod client; pub mod client_settings; pub(crate) mod encryption_settings; -pub mod kdf; pub use access_token::AccessToken; pub use client::Client; diff --git a/crates/bitwarden/src/crypto/enc_string/asymmetric.rs b/crates/bitwarden/src/crypto/enc_string/asymmetric.rs deleted file mode 100644 index 3c67c166a..000000000 --- a/crates/bitwarden/src/crypto/enc_string/asymmetric.rs +++ /dev/null @@ -1,250 +0,0 @@ -use std::{fmt::Display, str::FromStr}; - -use base64::{engine::general_purpose::STANDARD, Engine}; -#[cfg(feature = "internal")] -use rsa::{Oaep, RsaPrivateKey}; -use serde::Deserialize; - -use crate::error::{EncStringParseError, Error, Result}; - -#[cfg(feature = "internal")] -use crate::error::CryptoError; - -use super::{from_b64_vec, split_enc_string}; - -/// # Encrypted string primitive -/// -/// [AsymmEncString] is a Bitwarden specific primitive that represents an asymmetrically encrypted string. They are -/// are used together with the KeyDecryptable and KeyEncryptable traits to encrypt and decrypt -/// data using AsymmetricCryptoKeys. -/// -/// The flexibility of the [AsymmEncString] type allows for different encryption algorithms to be used -/// which is represented by the different variants of the enum. -/// -/// ## Note -/// -/// For backwards compatibility we will rarely if ever be able to remove support for decrypting old -/// variants, but we should be opinionated in which variants are used for encrypting. -/// -/// ## Variants -/// - [Rsa2048_OaepSha256_B64](AsymmEncString::Rsa2048_OaepSha256_B64) -/// - [Rsa2048_OaepSha1_B64](AsymmEncString::Rsa2048_OaepSha1_B64) -/// -/// ## Serialization -/// -/// [AsymmEncString] implements [Display] and [FromStr] to allow for easy serialization and uses a -/// custom scheme to represent the different variants. -/// -/// The scheme is one of the following schemes: -/// - `[type].[data]` -/// -/// Where: -/// - `[type]`: is a digit number representing the variant. -/// - `[data]`: is the encrypted data. -#[derive(Clone)] -#[allow(unused, non_camel_case_types)] -pub enum AsymmEncString { - /// 3 - Rsa2048_OaepSha256_B64 { data: Vec }, - /// 4 - Rsa2048_OaepSha1_B64 { data: Vec }, - /// 5 - #[deprecated] - Rsa2048_OaepSha256_HmacSha256_B64 { data: Vec, mac: Vec }, - /// 6 - #[deprecated] - Rsa2048_OaepSha1_HmacSha256_B64 { data: Vec, mac: Vec }, -} - -/// To avoid printing sensitive information, [AsymmEncString] debug prints to `AsymmEncString`. -impl std::fmt::Debug for AsymmEncString { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - f.debug_struct("AsymmEncString").finish() - } -} - -/// Deserializes an [AsymmEncString] from a string. -impl FromStr for AsymmEncString { - type Err = Error; - - fn from_str(s: &str) -> Result { - let (enc_type, parts) = split_enc_string(s); - match (enc_type, parts.len()) { - ("3", 1) => { - let data = from_b64_vec(parts[0])?; - Ok(AsymmEncString::Rsa2048_OaepSha256_B64 { data }) - } - ("4", 1) => { - let data = from_b64_vec(parts[0])?; - Ok(AsymmEncString::Rsa2048_OaepSha1_B64 { data }) - } - #[allow(deprecated)] - ("5", 2) => { - let data = from_b64_vec(parts[0])?; - let mac: Vec = from_b64_vec(parts[1])?; - Ok(AsymmEncString::Rsa2048_OaepSha256_HmacSha256_B64 { data, mac }) - } - #[allow(deprecated)] - ("6", 2) => { - let data = from_b64_vec(parts[0])?; - let mac: Vec = from_b64_vec(parts[1])?; - Ok(AsymmEncString::Rsa2048_OaepSha1_HmacSha256_B64 { data, mac }) - } - - (enc_type, parts) => Err(EncStringParseError::InvalidTypeAsymm { - enc_type: enc_type.to_string(), - parts, - } - .into()), - } - } -} - -#[allow(unused)] -impl AsymmEncString { - /// TODO: Convert this to a trait method - #[cfg(feature = "internal")] - pub(crate) fn decrypt(&self, key: &RsaPrivateKey) -> Result> { - Ok(match self { - Self::Rsa2048_OaepSha256_B64 { data } => key.decrypt(Oaep::new::(), data), - Self::Rsa2048_OaepSha1_B64 { data } => key.decrypt(Oaep::new::(), data), - #[allow(deprecated)] - Self::Rsa2048_OaepSha256_HmacSha256_B64 { data, mac: _ } => { - key.decrypt(Oaep::new::(), data) - } - #[allow(deprecated)] - Self::Rsa2048_OaepSha1_HmacSha256_B64 { data, mac: _ } => { - key.decrypt(Oaep::new::(), data) - } - } - .map_err(|_| CryptoError::KeyDecrypt)?) - } -} - -impl Display for AsymmEncString { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - let parts: Vec<&[u8]> = match self { - AsymmEncString::Rsa2048_OaepSha256_B64 { data } => vec![data], - AsymmEncString::Rsa2048_OaepSha1_B64 { data } => vec![data], - #[allow(deprecated)] - AsymmEncString::Rsa2048_OaepSha256_HmacSha256_B64 { data, mac } => vec![data, mac], - #[allow(deprecated)] - AsymmEncString::Rsa2048_OaepSha1_HmacSha256_B64 { data, mac } => vec![data, mac], - }; - - let encoded_parts: Vec = parts.iter().map(|part| STANDARD.encode(part)).collect(); - - write!(f, "{}.{}", self.enc_type(), encoded_parts.join("|"))?; - - Ok(()) - } -} - -impl<'de> Deserialize<'de> for AsymmEncString { - fn deserialize(deserializer: D) -> Result - where - D: serde::Deserializer<'de>, - { - deserializer.deserialize_str(super::FromStrVisitor::new()) - } -} - -impl serde::Serialize for AsymmEncString { - fn serialize(&self, serializer: S) -> Result - where - S: serde::Serializer, - { - serializer.serialize_str(&self.to_string()) - } -} - -impl AsymmEncString { - /// The numerical representation of the encryption type of the [AsymmEncString]. - const fn enc_type(&self) -> u8 { - match self { - AsymmEncString::Rsa2048_OaepSha256_B64 { .. } => 3, - AsymmEncString::Rsa2048_OaepSha1_B64 { .. } => 4, - #[allow(deprecated)] - AsymmEncString::Rsa2048_OaepSha256_HmacSha256_B64 { .. } => 5, - #[allow(deprecated)] - AsymmEncString::Rsa2048_OaepSha1_HmacSha256_B64 { .. } => 6, - } - } -} - -/// Usually we wouldn't want to expose AsymmEncStrings in the API or the schemas. -/// But during the transition phase we will expose endpoints using the AsymmEncString type. -impl schemars::JsonSchema for AsymmEncString { - fn schema_name() -> String { - "AsymmEncString".to_string() - } - - fn json_schema(gen: &mut schemars::gen::SchemaGenerator) -> schemars::schema::Schema { - gen.subschema_for::() - } -} - -#[cfg(test)] -mod tests { - use super::AsymmEncString; - - #[cfg(feature = "internal")] - #[test] - fn test_enc_string_rsa2048_oaep_sha1_hmac_sha256_b64() { - use rsa::{pkcs8::DecodePrivateKey, RsaPrivateKey}; - - let rsa_private_key: &str = "-----BEGIN PRIVATE KEY----- -MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQCXRVrCX+2hfOQS -8HzYUS2oc/jGVTZpv+/Ryuoh9d8ihYX9dd0cYh2tl6KWdFc88lPUH11Oxqy20Rk2 -e5r/RF6T9yM0Me3NPnaKt+hlhLtfoc0h86LnhD56A9FDUfuI0dVnPcrwNv0YJIo9 -4LwxtbqBULNvXl6wJ7WAbODrCQy5ZgMVg+iH+gGpwiqsZqHt+KuoHWcN53MSPDfa -F4/YMB99U3TziJMOOJask1TEEnakMPln11PczNDazT17DXIxYrbPfutPdh6sLs6A -QOajdZijfEvepgnOe7cQ7aeatiOJFrjTApKPGxOVRzEMX4XS4xbyhH0QxQeB6l16 -l8C0uxIBAgMBAAECggEASaWfeVDA3cVzOPFSpvJm20OTE+R6uGOU+7vh36TX/POq -92qBuwbd0h0oMD32FxsXywd2IxtBDUSiFM9699qufTVuM0Q3tZw6lHDTOVG08+tP -dr8qSbMtw7PGFxN79fHLBxejjO4IrM9lapjWpxEF+11x7r+wM+0xRZQ8sNFYG46a -PfIaty4BGbL0I2DQ2y8I57iBCAy69eht59NLMm27fRWGJIWCuBIjlpfzET1j2HLX -UIh5bTBNzqaN039WH49HczGE3mQKVEJZc/efk3HaVd0a1Sjzyn0QY+N1jtZN3jTR -buDWA1AknkX1LX/0tUhuS3/7C3ejHxjw4Dk1ZLo5/QKBgQDIWvqFn0+IKRSu6Ua2 -hDsufIHHUNLelbfLUMmFthxabcUn4zlvIscJO00Tq/ezopSRRvbGiqnxjv/mYxuc -vOUBeZtlus0Q9RTACBtw9TGoNTmQbEunJ2FOSlqbQxkBBAjgGEppRPt30iGj/VjA -hCATq2MYOa/X4dVR51BqQAFIEwKBgQDBSIfTFKC/hDk6FKZlgwvupWYJyU9Rkyfs -tPErZFmzoKhPkQ3YORo2oeAYmVUbS9I2iIYpYpYQJHX8jMuCbCz4ONxTCuSIXYQY -UcUq4PglCKp31xBAE6TN8SvhfME9/MvuDssnQinAHuF0GDAhF646T3LLS1not6Vs -zv7brwSoGwKBgQC88v/8cGfi80ssQZeMnVvq1UTXIeQcQnoY5lGHJl3K8mbS3TnX -E6c9j417Fdz+rj8KWzBzwWXQB5pSPflWcdZO886Xu/mVGmy9RWgLuVFhXwCwsVEP -jNX5ramRb0/vY0yzenUCninBsIxFSbIfrPtLUYCc4hpxr+sr2Mg/y6jpvQKBgBez -MRRs3xkcuXepuI2R+BCXL1/b02IJTUf1F+1eLLGd7YV0H+J3fgNc7gGWK51hOrF9 -JBZHBGeOUPlaukmPwiPdtQZpu4QNE3l37VlIpKTF30E6mb+BqR+nht3rUjarnMXg -AoEZ18y6/KIjpSMpqC92Nnk/EBM9EYe6Cf4eA9ApAoGAeqEUg46UTlJySkBKURGp -Is3v1kkf5I0X8DnOhwb+HPxNaiEdmO7ckm8+tPVgppLcG0+tMdLjigFQiDUQk2y3 -WjyxP5ZvXu7U96jaJRI8PFMoE06WeVYcdIzrID2HvqH+w0UQJFrLJ/0Mn4stFAEz -XKZBokBGnjFnTnKcs7nv/O8= ------END PRIVATE KEY-----"; - let private_key = RsaPrivateKey::from_pkcs8_pem(rsa_private_key).unwrap(); - let enc_str: &str = "6.ThnNc67nNr7GELyuhGGfsXNP2zJnNqhrIsjntEQ27r2qmn8vwdHbTbfO0cwt6YgSibDN0PjiCZ1O3Wb/IFq+vwvyRwFqF9145wBF8CQCbkhV+M0XvO99kh0daovtt120Nve/5ETI5PbPag9VdalKRQWZypJaqQHm5TAQVf4F5wtLlCLMBkzqTk+wkFe7BPMTGn07T+O3eJbTxXvyMZewQ7icJF0MZVA7VyWX9qElmZ89FCKowbf1BMr5pbcQ+0KdXcSVW3to43VkTp7k7COwsuH3M/i1AuVP5YN8ixjyRpvaeGqX/ap2nCHK2Wj5VxgCGT7XEls6ZknnAp9nB9qVjQ==|s3ntw5H/KKD/qsS0lUghTHl5Sm9j6m7YEdNHf0OeAFQ="; - let enc_string: AsymmEncString = enc_str.parse().unwrap(); - - assert_eq!(enc_string.enc_type(), 6); - - let res = enc_string.decrypt(&private_key).unwrap(); - - assert_eq!(std::str::from_utf8(&res).unwrap(), "EncryptMe!"); - } - - #[test] - fn test_enc_string_serialization() { - #[derive(serde::Serialize, serde::Deserialize)] - struct Test { - key: AsymmEncString, - } - - let cipher = "6.ThnNc67nNr7GELyuhGGfsXNP2zJnNqhrIsjntEQ27r2qmn8vwdHbTbfO0cwt6YgSibDN0PjiCZ1O3Wb/IFq+vwvyRwFqF9145wBF8CQCbkhV+M0XvO99kh0daovtt120Nve/5ETI5PbPag9VdalKRQWZypJaqQHm5TAQVf4F5wtLlCLMBkzqTk+wkFe7BPMTGn07T+O3eJbTxXvyMZewQ7icJF0MZVA7VyWX9qElmZ89FCKowbf1BMr5pbcQ+0KdXcSVW3to43VkTp7k7COwsuH3M/i1AuVP5YN8ixjyRpvaeGqX/ap2nCHK2Wj5VxgCGT7XEls6ZknnAp9nB9qVjQ==|s3ntw5H/KKD/qsS0lUghTHl5Sm9j6m7YEdNHf0OeAFQ="; - let serialized = format!("{{\"key\":\"{cipher}\"}}"); - - let t = serde_json::from_str::(&serialized).unwrap(); - assert_eq!(t.key.enc_type(), 6); - assert_eq!(t.key.to_string(), cipher); - assert_eq!(serde_json::to_string(&t).unwrap(), serialized); - } -} diff --git a/crates/bitwarden/src/crypto/key_encryptable.rs b/crates/bitwarden/src/crypto/key_encryptable.rs deleted file mode 100644 index 99c610bb2..000000000 --- a/crates/bitwarden/src/crypto/key_encryptable.rs +++ /dev/null @@ -1,69 +0,0 @@ -use std::{collections::HashMap, hash::Hash}; - -use crate::error::Result; - -use super::SymmetricCryptoKey; - -pub trait KeyEncryptable { - fn encrypt_with_key(self, key: &SymmetricCryptoKey) -> Result; -} - -pub trait KeyDecryptable { - fn decrypt_with_key(&self, key: &SymmetricCryptoKey) -> Result; -} - -impl, Output> KeyEncryptable> for Option { - fn encrypt_with_key(self, key: &SymmetricCryptoKey) -> Result> { - self.map(|e| e.encrypt_with_key(key)).transpose() - } -} - -impl, Output> KeyDecryptable> for Option { - fn decrypt_with_key(&self, key: &SymmetricCryptoKey) -> Result> { - self.as_ref().map(|e| e.decrypt_with_key(key)).transpose() - } -} - -impl, Output> KeyEncryptable for Box { - fn encrypt_with_key(self, key: &SymmetricCryptoKey) -> Result { - (*self).encrypt_with_key(key) - } -} - -impl, Output> KeyDecryptable for Box { - fn decrypt_with_key(&self, key: &SymmetricCryptoKey) -> Result { - (**self).decrypt_with_key(key) - } -} - -impl, Output> KeyEncryptable> for Vec { - fn encrypt_with_key(self, key: &SymmetricCryptoKey) -> Result> { - self.into_iter().map(|e| e.encrypt_with_key(key)).collect() - } -} - -impl, Output> KeyDecryptable> for Vec { - fn decrypt_with_key(&self, key: &SymmetricCryptoKey) -> Result> { - self.iter().map(|e| e.decrypt_with_key(key)).collect() - } -} - -impl, Output, Id: Hash + Eq> KeyEncryptable> - for HashMap -{ - fn encrypt_with_key(self, key: &SymmetricCryptoKey) -> Result> { - self.into_iter() - .map(|(id, e)| Ok((id, e.encrypt_with_key(key)?))) - .collect() - } -} - -impl, Output, Id: Hash + Eq + Copy> KeyDecryptable> - for HashMap -{ - fn decrypt_with_key(&self, key: &SymmetricCryptoKey) -> Result> { - self.iter() - .map(|(id, e)| Ok((*id, e.decrypt_with_key(key)?))) - .collect() - } -} diff --git a/crates/bitwarden/src/crypto/mod.rs b/crates/bitwarden/src/crypto/mod.rs deleted file mode 100644 index 94db974d3..000000000 --- a/crates/bitwarden/src/crypto/mod.rs +++ /dev/null @@ -1,87 +0,0 @@ -//! # Cryptographic primitives -//! -//! This module contains the cryptographic primitives used throughout the SDK. The module makes a -//! best effort to abstract away cryptographic concepts into concepts such as -//! [`EncString`] and [`SymmetricCryptoKey`]. -//! -//! ## Conventions: -//! -//! - Pure Functions that deterministically "derive" keys from input are prefixed with `derive_`. -//! - Functions that generate new keys are prefixed with `make_`. -//! -//! ## Differences from [`clients`](https://github.com/bitwarden/clients) -//! -//! There are some noteworthy differences compared to the other Bitwarden clients. These changes -//! are made in an effort to introduce conventions in how we name things, improve best practices -//! and abstracting away internal complexity. -//! -//! - `CryptoService.makeSendKey` & `AccessService.createAccessToken` are replaced by the generic -//! `derive_shareable_key` -//! - MasterKey operations such as `makeMasterKey` and `hashMasterKey` are moved to the MasterKey -//! struct. -//! - -use aes::cipher::{generic_array::GenericArray, ArrayLength, Unsigned}; -use hmac::digest::OutputSizeUser; -use rand::{ - distributions::{Distribution, Standard}, - Rng, -}; - -use crate::error::Result; - -mod enc_string; -pub use enc_string::{AsymmEncString, EncString}; -mod encryptable; -pub use encryptable::{Decryptable, Encryptable, LocateKey}; -mod key_encryptable; -pub use key_encryptable::{KeyDecryptable, KeyEncryptable}; -mod aes_ops; -use aes_ops::{decrypt_aes256_hmac, encrypt_aes256_hmac}; -mod symmetric_crypto_key; -pub use symmetric_crypto_key::SymmetricCryptoKey; -mod shareable_key; -pub(crate) use shareable_key::derive_shareable_key; - -#[cfg(feature = "internal")] -mod master_key; -#[cfg(feature = "internal")] -pub use master_key::HashPurpose; -#[cfg(feature = "internal")] -pub(crate) use master_key::MasterKey; -#[cfg(feature = "internal")] -mod user_key; -#[cfg(feature = "internal")] -pub(crate) use user_key::UserKey; -#[cfg(feature = "internal")] -mod rsa; -#[cfg(feature = "internal")] -pub use self::rsa::RsaKeyPair; - -#[cfg(feature = "internal")] -mod fingerprint; -#[cfg(feature = "internal")] -pub(crate) use fingerprint::fingerprint; - -pub(crate) type PbkdfSha256Hmac = hmac::Hmac; -pub(crate) const PBKDF_SHA256_HMAC_OUT_SIZE: usize = - <::OutputSize as Unsigned>::USIZE; - -/// RFC5869 HKDF-Expand operation -fn hkdf_expand>(prk: &[u8], info: Option<&str>) -> Result> { - let hkdf = hkdf::Hkdf::::from_prk(prk).map_err(|_| "invalid prk length")?; - let mut key = GenericArray::::default(); - - let i = info.map(|i| i.as_bytes()).unwrap_or(&[]); - hkdf.expand(i, &mut key).map_err(|_| "invalid length")?; - - Ok(key) -} - -/// Generate random bytes that are cryptographically secure -pub(crate) fn generate_random_bytes() -> T -where - Standard: Distribution, -{ - rand::thread_rng().gen() -} diff --git a/crates/bitwarden/src/crypto/symmetric_crypto_key.rs b/crates/bitwarden/src/crypto/symmetric_crypto_key.rs deleted file mode 100644 index f6eda5e48..000000000 --- a/crates/bitwarden/src/crypto/symmetric_crypto_key.rs +++ /dev/null @@ -1,101 +0,0 @@ -use std::str::FromStr; - -use aes::cipher::{generic_array::GenericArray, typenum::U32}; -use base64::{engine::general_purpose::STANDARD, Engine}; - -use crate::{ - crypto::{derive_shareable_key, generate_random_bytes}, - error::{CryptoError, Error}, -}; - -/// A symmetric encryption key. Used to encrypt and decrypt [`EncString`](crate::crypto::EncString) -pub struct SymmetricCryptoKey { - pub key: GenericArray, - pub mac_key: Option>, -} - -impl SymmetricCryptoKey { - const KEY_LEN: usize = 32; - const MAC_LEN: usize = 32; - - pub fn generate(name: &str) -> Self { - let secret: [u8; 16] = generate_random_bytes(); - derive_shareable_key(secret, name, None) - } - - pub fn to_base64(&self) -> String { - let mut buf = Vec::new(); - buf.extend_from_slice(&self.key); - - if let Some(mac) = self.mac_key { - buf.extend_from_slice(&mac); - } - - STANDARD.encode(&buf) - } - - #[cfg(feature = "internal")] - pub(super) fn to_vec(&self) -> Vec { - let mut buf = Vec::new(); - buf.extend_from_slice(&self.key); - if let Some(mac) = self.mac_key { - buf.extend_from_slice(&mac); - } - buf - } -} - -impl FromStr for SymmetricCryptoKey { - type Err = Error; - - fn from_str(s: &str) -> Result { - let bytes = STANDARD.decode(s).map_err(|_| CryptoError::InvalidKey)?; - SymmetricCryptoKey::try_from(bytes.as_slice()) - } -} - -impl TryFrom<&[u8]> for SymmetricCryptoKey { - type Error = Error; - - fn try_from(value: &[u8]) -> Result { - if value.len() == Self::KEY_LEN + Self::MAC_LEN { - Ok(SymmetricCryptoKey { - key: GenericArray::clone_from_slice(&value[..Self::KEY_LEN]), - mac_key: Some(GenericArray::clone_from_slice(&value[Self::KEY_LEN..])), - }) - } else if value.len() == Self::KEY_LEN { - Ok(SymmetricCryptoKey { - key: GenericArray::clone_from_slice(value), - mac_key: None, - }) - } else { - Err(CryptoError::InvalidKeyLen.into()) - } - } -} - -// We manually implement these to make sure we don't print any sensitive data -impl std::fmt::Debug for SymmetricCryptoKey { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - f.debug_struct("Key").finish() - } -} - -#[cfg(test)] -mod tests { - use std::str::FromStr; - - use super::SymmetricCryptoKey; - - #[test] - fn test_symmetric_crypto_key() { - let key = SymmetricCryptoKey::generate("test"); - let key2 = SymmetricCryptoKey::from_str(&key.to_base64()).unwrap(); - assert_eq!(key.key, key2.key); - assert_eq!(key.mac_key, key2.mac_key); - - let key = "UY4B5N4DA4UisCNClgZtRr6VLy9ZF5BXXC7cDZRqourKi4ghEMgISbCsubvgCkHf5DZctQjVot11/vVvN9NNHQ=="; - let key2 = SymmetricCryptoKey::from_str(key).unwrap(); - assert_eq!(key, key2.to_base64()); - } -} diff --git a/crates/bitwarden/src/crypto/user_key.rs b/crates/bitwarden/src/crypto/user_key.rs deleted file mode 100644 index 7f5bae413..000000000 --- a/crates/bitwarden/src/crypto/user_key.rs +++ /dev/null @@ -1,19 +0,0 @@ -use crate::{ - crypto::{ - rsa::{make_key_pair, RsaKeyPair}, - SymmetricCryptoKey, - }, - error::Result, -}; - -pub(crate) struct UserKey(pub(super) SymmetricCryptoKey); - -impl UserKey { - pub(crate) fn new(key: SymmetricCryptoKey) -> Self { - Self(key) - } - - pub(crate) fn make_key_pair(&self) -> Result { - make_key_pair(&self.0) - } -} diff --git a/crates/bitwarden/src/error.rs b/crates/bitwarden/src/error.rs index 48cb7da16..173557b04 100644 --- a/crates/bitwarden/src/error.rs +++ b/crates/bitwarden/src/error.rs @@ -4,6 +4,7 @@ use std::{borrow::Cow, fmt::Debug}; use bitwarden_api_api::apis::Error as ApiError; use bitwarden_api_identity::apis::Error as IdentityError; +use bitwarden_generators::{PassphraseError, PasswordError, UsernameError}; use reqwest::StatusCode; use thiserror::Error; @@ -24,10 +25,7 @@ pub enum Error { MissingFields, #[error("Cryptography error, {0}")] - Crypto(#[from] CryptoError), - - #[error("Error parsing EncString: {0}")] - InvalidEncString(#[from] EncStringParseError), + Crypto(#[from] bitwarden_crypto::CryptoError), #[error("Error parsing Identity response: {0}")] IdentityFail(crate::auth::api::response::IdentityTokenFailResponse), @@ -52,6 +50,13 @@ pub enum Error { #[error("The state file could not be read")] InvalidStateFile, + #[error(transparent)] + UsernameError(#[from] UsernameError), + #[error(transparent)] + PassphraseError(#[from] PassphraseError), + #[error(transparent)] + PasswordError(#[from] PasswordError), + #[error("Internal error: {0}")] Internal(Cow<'static, str>), } @@ -86,38 +91,6 @@ pub enum AccessTokenInvalidError { InvalidBase64Length { expected: usize, got: usize }, } -#[derive(Debug, Error)] -pub enum CryptoError { - #[error("The provided key is not the expected type")] - InvalidKey, - #[error("The cipher's MAC doesn't match the expected value")] - InvalidMac, - #[error("Error while decrypting EncString")] - KeyDecrypt, - #[error("The cipher key has an invalid length")] - InvalidKeyLen, - #[error("There is no encryption key for the provided organization")] - NoKeyForOrg, - #[error("The value is not a valid UTF8 String")] - InvalidUtf8String, - #[error("Missing key")] - MissingKey, -} - -#[derive(Debug, Error)] -pub enum EncStringParseError { - #[error("No type detected, missing '.' separator")] - NoType, - #[error("Invalid symmetric type, got type {enc_type} with {parts} parts")] - InvalidTypeSymm { enc_type: String, parts: usize }, - #[error("Invalid asymmetric type, got type {enc_type} with {parts} parts")] - InvalidTypeAsymm { enc_type: String, parts: usize }, - #[error("Error decoding base64: {0}")] - InvalidBase64(#[from] base64::DecodeError), - #[error("Invalid length: expected {expected}, got {got}")] - InvalidLength { expected: usize, got: usize }, -} - // Ensure that the error messages implement Send and Sync #[cfg(test)] const _: () = { diff --git a/crates/bitwarden/src/lib.rs b/crates/bitwarden/src/lib.rs index a8dd60399..28eb521de 100644 --- a/crates/bitwarden/src/lib.rs +++ b/crates/bitwarden/src/lib.rs @@ -46,7 +46,6 @@ //! Ok(()) //! } //! ``` -//! #[cfg(feature = "mobile")] uniffi::setup_scaffolding!(); @@ -55,7 +54,6 @@ uniffi::setup_scaffolding!(); pub mod admin_console; pub mod auth; pub mod client; -pub mod crypto; pub mod error; #[cfg(feature = "mobile")] pub mod mobile; @@ -70,10 +68,15 @@ pub(crate) mod uniffi_support; mod util; #[cfg(feature = "internal")] pub mod vault; -pub mod wordlist; pub use client::Client; // Ensure the readme docs compile #[doc = include_str!("../README.md")] mod readme {} + +pub mod generators { + pub use bitwarden_generators::{ + PassphraseGeneratorRequest, PasswordGeneratorRequest, UsernameGeneratorRequest, + }; +} diff --git a/crates/bitwarden/src/mobile/client_crypto.rs b/crates/bitwarden/src/mobile/client_crypto.rs index cddb7c140..6f8887002 100644 --- a/crates/bitwarden/src/mobile/client_crypto.rs +++ b/crates/bitwarden/src/mobile/client_crypto.rs @@ -1,10 +1,13 @@ +#[cfg(feature = "internal")] +use bitwarden_crypto::EncString; + use crate::Client; #[cfg(feature = "internal")] use crate::{ error::Result, mobile::crypto::{ - derive_pin_key, get_user_encryption_key, initialize_org_crypto, initialize_user_crypto, - DerivePinKeyResponse, InitOrgCryptoRequest, InitUserCryptoRequest, + derive_pin_key, derive_pin_user_key, get_user_encryption_key, initialize_org_crypto, + initialize_user_crypto, DerivePinKeyResponse, InitOrgCryptoRequest, InitUserCryptoRequest, }, }; @@ -32,6 +35,11 @@ impl<'a> ClientCrypto<'a> { pub async fn derive_pin_key(&mut self, pin: String) -> Result { derive_pin_key(self.client, pin) } + + #[cfg(feature = "internal")] + pub async fn derive_pin_user_key(&mut self, encrypted_pin: EncString) -> Result { + derive_pin_user_key(self.client, encrypted_pin) + } } impl<'a> Client { diff --git a/crates/bitwarden/src/mobile/client_kdf.rs b/crates/bitwarden/src/mobile/client_kdf.rs index 6b895f78a..4e62e5d59 100644 --- a/crates/bitwarden/src/mobile/client_kdf.rs +++ b/crates/bitwarden/src/mobile/client_kdf.rs @@ -1,6 +1,6 @@ -use crate::{ - client::kdf::Kdf, crypto::HashPurpose, error::Result, mobile::kdf::hash_password, Client, -}; +use bitwarden_crypto::HashPurpose; + +use crate::{client::Kdf, error::Result, mobile::kdf::hash_password, Client}; pub struct ClientKdf<'a> { pub(crate) client: &'a crate::Client, diff --git a/crates/bitwarden/src/mobile/crypto.rs b/crates/bitwarden/src/mobile/crypto.rs index ca5dea703..bd71af2be 100644 --- a/crates/bitwarden/src/mobile/crypto.rs +++ b/crates/bitwarden/src/mobile/crypto.rs @@ -1,11 +1,15 @@ use std::collections::HashMap; +use bitwarden_crypto::{AsymmetricEncString, EncString}; +#[cfg(feature = "internal")] +use bitwarden_crypto::{KeyDecryptable, KeyEncryptable, MasterKey, SymmetricCryptoKey}; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; +#[cfg(feature = "internal")] +use crate::client::{LoginMethod, UserLoginMethod}; use crate::{ - client::kdf::Kdf, - crypto::{AsymmEncString, EncString}, + client::Kdf, error::{Error, Result}, Client, }; @@ -43,14 +47,21 @@ pub enum InitUserCryptoMethod { Pin { /// The user's PIN pin: String, - /// The user's symmetric crypto key, encrypted with the PIN. Use `derive_pin_key` to obtain this. + /// The user's symmetric crypto key, encrypted with the PIN. Use `derive_pin_key` to obtain + /// this. pin_protected_user_key: EncString, }, + AuthRequest { + /// Private Key generated by the `crate::auth::new_auth_request`. + request_private_key: String, + /// User Key protected by the private key provided in `AuthRequestResponse`. + protected_user_key: AsymmetricEncString, + }, } #[cfg(feature = "internal")] pub async fn initialize_user_crypto(client: &mut Client, req: InitUserCryptoRequest) -> Result<()> { - use crate::crypto::SymmetricCryptoKey; + use crate::auth::auth_request_decrypt_user_key; let login_method = crate::client::LoginMethod::User(crate::client::UserLoginMethod::Username { client_id: "".to_string(), @@ -76,6 +87,13 @@ pub async fn initialize_user_crypto(client: &mut Client, req: InitUserCryptoRequ } => { client.initialize_user_crypto_pin(&pin, pin_protected_user_key, private_key)?; } + InitUserCryptoMethod::AuthRequest { + request_private_key, + protected_user_key, + } => { + let user_key = auth_request_decrypt_user_key(request_private_key, protected_user_key)?; + client.initialize_user_crypto_decrypted_key(user_key, private_key)?; + } } Ok(()) @@ -87,7 +105,7 @@ pub async fn initialize_user_crypto(client: &mut Client, req: InitUserCryptoRequ #[cfg_attr(feature = "mobile", derive(uniffi::Record))] pub struct InitOrgCryptoRequest { /// The encryption keys for all the organizations the user is a part of - pub organization_keys: HashMap, + pub organization_keys: HashMap, } #[cfg(feature = "internal")] @@ -112,40 +130,69 @@ pub async fn get_user_encryption_key(client: &mut Client) -> Result { #[serde(rename_all = "camelCase", deny_unknown_fields)] #[cfg_attr(feature = "mobile", derive(uniffi::Record))] pub struct DerivePinKeyResponse { + /// [UserKey] protected by PIN pin_protected_user_key: EncString, + /// PIN protected by [UserKey] encrypted_pin: EncString, } #[cfg(feature = "internal")] pub fn derive_pin_key(client: &mut Client, pin: String) -> Result { - use crate::{ - client::{LoginMethod, UserLoginMethod}, - crypto::{KeyEncryptable, MasterKey}, - }; - - let derived_key = match &client.login_method { - Some(LoginMethod::User( - UserLoginMethod::Username { email, kdf, .. } - | UserLoginMethod::ApiKey { email, kdf, .. }, - )) => MasterKey::derive(pin.as_bytes(), email.as_bytes(), kdf)?, - _ => return Err(Error::NotAuthenticated), - }; - let user_key = client .get_encryption_settings()? .get_key(&None) .ok_or(Error::VaultLocked)?; + let login_method = client + .login_method + .as_ref() + .ok_or(Error::NotAuthenticated)?; + + let pin_protected_user_key = derive_pin_protected_user_key(&pin, login_method, user_key)?; + Ok(DerivePinKeyResponse { - pin_protected_user_key: derived_key.encrypt_user_key(user_key)?, + pin_protected_user_key, encrypted_pin: pin.encrypt_with_key(user_key)?, }) } +#[cfg(feature = "internal")] +pub fn derive_pin_user_key(client: &mut Client, encrypted_pin: EncString) -> Result { + let user_key = client + .get_encryption_settings()? + .get_key(&None) + .ok_or(Error::VaultLocked)?; + + let pin: String = encrypted_pin.decrypt_with_key(user_key)?; + let login_method = client + .login_method + .as_ref() + .ok_or(Error::NotAuthenticated)?; + + derive_pin_protected_user_key(&pin, login_method, user_key) +} + +#[cfg(feature = "internal")] +fn derive_pin_protected_user_key( + pin: &str, + login_method: &LoginMethod, + user_key: &SymmetricCryptoKey, +) -> Result { + let derived_key = match login_method { + LoginMethod::User( + UserLoginMethod::Username { email, kdf, .. } + | UserLoginMethod::ApiKey { email, kdf, .. }, + ) => MasterKey::derive(pin.as_bytes(), email.as_bytes(), kdf)?, + _ => return Err(Error::NotAuthenticated), + }; + + Ok(derived_key.encrypt_user_key(user_key)?) +} + #[cfg(test)] mod tests { use super::*; - use crate::{client::kdf::Kdf, Client}; + use crate::{client::Kdf, Client}; #[tokio::test] async fn test_initialize_user_crypto_pin() { @@ -172,8 +219,8 @@ mod tests { let pin_key = derive_pin_key(&mut client, "1234".into()).unwrap(); + // Verify we can unlock with the pin let mut client2 = Client::new(None); - initialize_user_crypto( &mut client2, InitUserCryptoRequest { @@ -205,5 +252,43 @@ mod tests { .unwrap() .to_base64() ); + + // Verify we can derive the pin protected user key from the encrypted pin + let pin_protected_user_key = + derive_pin_user_key(&mut client, pin_key.encrypted_pin).unwrap(); + + let mut client3 = Client::new(None); + + initialize_user_crypto( + &mut client3, + InitUserCryptoRequest { + kdf_params: Kdf::PBKDF2 { + iterations: 100_000.try_into().unwrap(), + }, + email: "test@bitwarden.com".into(), + private_key: priv_key.to_owned(), + method: InitUserCryptoMethod::Pin { + pin: "1234".into(), + pin_protected_user_key, + }, + }, + ) + .await + .unwrap(); + + assert_eq!( + client + .get_encryption_settings() + .unwrap() + .get_key(&None) + .unwrap() + .to_base64(), + client3 + .get_encryption_settings() + .unwrap() + .get_key(&None) + .unwrap() + .to_base64() + ); } } diff --git a/crates/bitwarden/src/mobile/kdf.rs b/crates/bitwarden/src/mobile/kdf.rs index de2fcc79f..1c1972086 100644 --- a/crates/bitwarden/src/mobile/kdf.rs +++ b/crates/bitwarden/src/mobile/kdf.rs @@ -1,9 +1,6 @@ -use crate::{ - client::kdf::Kdf, - crypto::{HashPurpose, MasterKey}, - error::Result, - Client, -}; +use bitwarden_crypto::{HashPurpose, Kdf, MasterKey}; + +use crate::{error::Result, Client}; pub async fn hash_password( _client: &Client, @@ -14,5 +11,5 @@ pub async fn hash_password( ) -> Result { let master_key = MasterKey::derive(password.as_bytes(), email.as_bytes(), &kdf_params)?; - master_key.derive_master_key_hash(password.as_bytes(), purpose) + Ok(master_key.derive_master_key_hash(password.as_bytes(), purpose)?) } diff --git a/crates/bitwarden/src/mobile/vault/client_attachments.rs b/crates/bitwarden/src/mobile/vault/client_attachments.rs new file mode 100644 index 000000000..c436f10fd --- /dev/null +++ b/crates/bitwarden/src/mobile/vault/client_attachments.rs @@ -0,0 +1,89 @@ +use std::path::Path; + +use bitwarden_crypto::{EncString, KeyDecryptable, KeyEncryptable, LocateKey}; + +use super::client_vault::ClientVault; +use crate::{ + error::{Error, Result}, + vault::{ + Attachment, AttachmentEncryptResult, AttachmentFile, AttachmentFileView, AttachmentView, + Cipher, + }, + Client, +}; + +pub struct ClientAttachments<'a> { + pub(crate) client: &'a Client, +} + +impl<'a> ClientAttachments<'a> { + pub async fn encrypt_buffer( + &self, + cipher: Cipher, + attachment: AttachmentView, + buffer: &[u8], + ) -> Result { + let enc = self.client.get_encryption_settings()?; + let key = cipher.locate_key(enc, &None).ok_or(Error::VaultLocked)?; + + Ok(AttachmentFileView { + cipher, + attachment, + contents: buffer, + } + .encrypt_with_key(key)?) + } + pub async fn encrypt_file( + &self, + cipher: Cipher, + attachment: AttachmentView, + decrypted_file_path: &Path, + encrypted_file_path: &Path, + ) -> Result { + let data = std::fs::read(decrypted_file_path).unwrap(); + let AttachmentEncryptResult { + attachment, + contents, + } = self.encrypt_buffer(cipher, attachment, &data).await?; + std::fs::write(encrypted_file_path, contents)?; + Ok(attachment) + } + + pub async fn decrypt_buffer( + &self, + cipher: Cipher, + attachment: Attachment, + encrypted_buffer: &[u8], + ) -> Result> { + let enc = self.client.get_encryption_settings()?; + let key = cipher.locate_key(enc, &None).ok_or(Error::VaultLocked)?; + + AttachmentFile { + cipher, + attachment, + contents: EncString::from_buffer(encrypted_buffer)?, + } + .decrypt_with_key(key) + .map_err(Error::Crypto) + } + pub async fn decrypt_file( + &self, + cipher: Cipher, + attachment: Attachment, + encrypted_file_path: &Path, + decrypted_file_path: &Path, + ) -> Result<()> { + let data = std::fs::read(encrypted_file_path).unwrap(); + let decrypted = self.decrypt_buffer(cipher, attachment, &data).await?; + std::fs::write(decrypted_file_path, decrypted)?; + Ok(()) + } +} + +impl<'a> ClientVault<'a> { + pub fn attachments(&'a self) -> ClientAttachments<'a> { + ClientAttachments { + client: self.client, + } + } +} diff --git a/crates/bitwarden/src/mobile/vault/client_ciphers.rs b/crates/bitwarden/src/mobile/vault/client_ciphers.rs index 87b77cfd5..4e34021ee 100644 --- a/crates/bitwarden/src/mobile/vault/client_ciphers.rs +++ b/crates/bitwarden/src/mobile/vault/client_ciphers.rs @@ -1,6 +1,7 @@ +use bitwarden_crypto::{Decryptable, Encryptable}; + use super::client_vault::ClientVault; use crate::{ - crypto::{Decryptable, Encryptable}, error::Result, vault::{Cipher, CipherListView, CipherView}, Client, diff --git a/crates/bitwarden/src/mobile/vault/client_collection.rs b/crates/bitwarden/src/mobile/vault/client_collection.rs index 80535406c..9cb5d1711 100644 --- a/crates/bitwarden/src/mobile/vault/client_collection.rs +++ b/crates/bitwarden/src/mobile/vault/client_collection.rs @@ -1,6 +1,7 @@ +use bitwarden_crypto::Decryptable; + use super::client_vault::ClientVault; use crate::{ - crypto::Decryptable, error::Result, vault::{Collection, CollectionView}, Client, diff --git a/crates/bitwarden/src/mobile/vault/client_folders.rs b/crates/bitwarden/src/mobile/vault/client_folders.rs index b6f6a72f4..fec3ad7db 100644 --- a/crates/bitwarden/src/mobile/vault/client_folders.rs +++ b/crates/bitwarden/src/mobile/vault/client_folders.rs @@ -1,6 +1,7 @@ +use bitwarden_crypto::{Decryptable, Encryptable}; + use super::client_vault::ClientVault; use crate::{ - crypto::{Decryptable, Encryptable}, error::Result, vault::{Folder, FolderView}, Client, diff --git a/crates/bitwarden/src/mobile/vault/client_password_history.rs b/crates/bitwarden/src/mobile/vault/client_password_history.rs index d873f8dfc..99727232b 100644 --- a/crates/bitwarden/src/mobile/vault/client_password_history.rs +++ b/crates/bitwarden/src/mobile/vault/client_password_history.rs @@ -1,6 +1,7 @@ +use bitwarden_crypto::{Decryptable, Encryptable}; + use super::client_vault::ClientVault; use crate::{ - crypto::{Decryptable, Encryptable}, error::Result, vault::{PasswordHistory, PasswordHistoryView}, Client, diff --git a/crates/bitwarden/src/mobile/vault/client_sends.rs b/crates/bitwarden/src/mobile/vault/client_sends.rs index b1de05f44..45d9a7825 100644 --- a/crates/bitwarden/src/mobile/vault/client_sends.rs +++ b/crates/bitwarden/src/mobile/vault/client_sends.rs @@ -1,8 +1,9 @@ use std::path::Path; +use bitwarden_crypto::{Decryptable, EncString, Encryptable, KeyDecryptable, KeyEncryptable}; + use super::client_vault::ClientVault; use crate::{ - crypto::{Decryptable, EncString, Encryptable, KeyDecryptable, KeyEncryptable}, error::{Error, Result}, vault::{Send, SendListView, SendView}, Client, @@ -47,7 +48,7 @@ impl<'a> ClientSends<'a> { let key = Send::get_key(&send.key, key)?; let buf = EncString::from_buffer(encrypted_buffer)?; - buf.decrypt_with_key(&key) + Ok(buf.decrypt_with_key(&key)?) } pub async fn encrypt(&self, send_view: SendView) -> Result { @@ -79,7 +80,7 @@ impl<'a> ClientSends<'a> { let key = Send::get_key(&send.key, key)?; let enc = buffer.encrypt_with_key(&key)?; - enc.to_buffer() + Ok(enc.to_buffer()?) } } diff --git a/crates/bitwarden/src/mobile/vault/client_totp.rs b/crates/bitwarden/src/mobile/vault/client_totp.rs index 75bfd204c..3d3b80f98 100644 --- a/crates/bitwarden/src/mobile/vault/client_totp.rs +++ b/crates/bitwarden/src/mobile/vault/client_totp.rs @@ -1,9 +1,10 @@ use chrono::{DateTime, Utc}; -use crate::error::Result; -use crate::vault::{generate_totp, TotpResponse}; - use super::client_vault::ClientVault; +use crate::{ + error::Result, + vault::{generate_totp, TotpResponse}, +}; impl<'a> ClientVault<'a> { /// Generate a TOTP code from a provided key. @@ -12,7 +13,6 @@ impl<'a> ClientVault<'a> { /// - A base32 encoded string /// - OTP Auth URI /// - Steam URI - /// pub fn generate_totp( &'a self, key: String, diff --git a/crates/bitwarden/src/mobile/vault/mod.rs b/crates/bitwarden/src/mobile/vault/mod.rs index 97f9556af..a2d4d91b3 100644 --- a/crates/bitwarden/src/mobile/vault/mod.rs +++ b/crates/bitwarden/src/mobile/vault/mod.rs @@ -1,3 +1,4 @@ +mod client_attachments; mod client_ciphers; mod client_collection; mod client_folders; @@ -6,6 +7,7 @@ mod client_sends; mod client_totp; mod client_vault; +pub use client_attachments::ClientAttachments; pub use client_ciphers::ClientCiphers; pub use client_collection::ClientCollections; pub use client_folders::ClientFolders; diff --git a/crates/bitwarden/src/platform/generate_fingerprint.rs b/crates/bitwarden/src/platform/generate_fingerprint.rs index cd763b765..59d81d652 100644 --- a/crates/bitwarden/src/platform/generate_fingerprint.rs +++ b/crates/bitwarden/src/platform/generate_fingerprint.rs @@ -1,10 +1,10 @@ use base64::{engine::general_purpose::STANDARD, Engine}; +use bitwarden_crypto::fingerprint; use log::info; -use rsa::pkcs8::EncodePublicKey; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; -use crate::{crypto::fingerprint, error::Result}; +use crate::error::Result; #[derive(Serialize, Deserialize, Debug, JsonSchema)] #[serde(rename_all = "camelCase", deny_unknown_fields)] @@ -44,12 +44,8 @@ pub(crate) fn generate_user_fingerprint( .as_ref() .ok_or("Missing private key")?; - let public_key = private_key - .to_public_key() - .to_public_key_der() - .map_err(|_| "Invalid key")?; - - let fingerprint = fingerprint(&fingerprint_material, public_key.as_bytes())?; + let public_key = private_key.to_public_der()?; + let fingerprint = fingerprint(&fingerprint_material, &public_key)?; Ok(fingerprint) } @@ -58,13 +54,12 @@ pub(crate) fn generate_user_fingerprint( mod tests { use std::num::NonZeroU32; + use super::*; use crate::{ - client::{kdf::Kdf, LoginMethod, UserLoginMethod}, + client::{Kdf, LoginMethod, UserLoginMethod}, Client, }; - use super::*; - #[test] fn test_generate_user_fingerprint() { let user_key = "2.oZg5RYpU2HjUAKI1DUQCkg==|PyRzI9kZpt66P2OedH8CHOeU0/lgKLkhIJiKDijdyFqIemBSIBoslhfQh/P1TK9xgZp0smgD6+5+yNbZfOpBaCVrsT3WWAO78xOWizduRe4=|xfDLDZSJ+yZAdh388flVg7SMDBJuMs0+CHTjutKs4uQ="; diff --git a/crates/bitwarden/src/platform/get_user_api_key.rs b/crates/bitwarden/src/platform/get_user_api_key.rs index 994a7a7ab..2eaa21894 100644 --- a/crates/bitwarden/src/platform/get_user_api_key.rs +++ b/crates/bitwarden/src/platform/get_user_api_key.rs @@ -2,6 +2,7 @@ use bitwarden_api_api::{ apis::accounts_api::accounts_api_key_post, models::{ApiKeyResponseModel, SecretVerificationRequestModel}, }; +use bitwarden_crypto::{HashPurpose, MasterKey}; use log::{debug, info}; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; @@ -9,7 +10,6 @@ use serde::{Deserialize, Serialize}; use super::SecretVerificationRequest; use crate::{ client::{LoginMethod, UserLoginMethod}, - crypto::{HashPurpose, MasterKey}, error::{Error, Result}, Client, }; diff --git a/crates/bitwarden/src/platform/secret_verification_request.rs b/crates/bitwarden/src/platform/secret_verification_request.rs index b501d0181..e05926620 100644 --- a/crates/bitwarden/src/platform/secret_verification_request.rs +++ b/crates/bitwarden/src/platform/secret_verification_request.rs @@ -4,10 +4,11 @@ use serde::{Deserialize, Serialize}; #[derive(Serialize, Deserialize, Debug, JsonSchema)] #[serde(rename_all = "camelCase", deny_unknown_fields)] pub struct SecretVerificationRequest { - /// The user's master password to use for user verification. If supplied, this will be used for verification - /// purposes. + /// The user's master password to use for user verification. If supplied, this will be used for + /// verification purposes. pub master_password: Option, - /// 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. + /// 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. pub otp: Option, } diff --git a/crates/bitwarden/src/platform/sync.rs b/crates/bitwarden/src/platform/sync.rs index 870b88ab9..c1a039137 100644 --- a/crates/bitwarden/src/platform/sync.rs +++ b/crates/bitwarden/src/platform/sync.rs @@ -69,7 +69,8 @@ pub struct DomainResponse { #[derive(Serialize, Deserialize, Debug, JsonSchema)] #[serde(rename_all = "camelCase", deny_unknown_fields)] pub struct SyncResponse { - /// Data about the user, including their encryption keys and the organizations they are a part of + /// Data about the user, including their encryption keys and the organizations they are a part + /// of pub profile: ProfileResponse, pub folders: Vec, pub collections: Vec, diff --git a/crates/bitwarden/src/secrets_manager/projects/create.rs b/crates/bitwarden/src/secrets_manager/projects/create.rs index 996a3463e..ab3b7bd62 100644 --- a/crates/bitwarden/src/secrets_manager/projects/create.rs +++ b/crates/bitwarden/src/secrets_manager/projects/create.rs @@ -1,4 +1,5 @@ use bitwarden_api_api::models::ProjectCreateRequestModel; +use bitwarden_crypto::KeyEncryptable; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; use uuid::Uuid; @@ -6,7 +7,6 @@ use uuid::Uuid; use super::ProjectResponse; use crate::{ client::Client, - crypto::KeyEncryptable, error::{Error, Result}, }; diff --git a/crates/bitwarden/src/secrets_manager/projects/project_response.rs b/crates/bitwarden/src/secrets_manager/projects/project_response.rs index b8c82806b..1e6f6a158 100644 --- a/crates/bitwarden/src/secrets_manager/projects/project_response.rs +++ b/crates/bitwarden/src/secrets_manager/projects/project_response.rs @@ -1,4 +1,5 @@ use bitwarden_api_api::models::ProjectResponseModel; +use bitwarden_crypto::{Decryptable, EncString}; use chrono::{DateTime, Utc}; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; @@ -6,7 +7,6 @@ use uuid::Uuid; use crate::{ client::encryption_settings::EncryptionSettings, - crypto::{Decryptable, EncString}, error::{Error, Result}, }; diff --git a/crates/bitwarden/src/secrets_manager/projects/update.rs b/crates/bitwarden/src/secrets_manager/projects/update.rs index 6a0479d88..e00609ff4 100644 --- a/crates/bitwarden/src/secrets_manager/projects/update.rs +++ b/crates/bitwarden/src/secrets_manager/projects/update.rs @@ -1,4 +1,5 @@ use bitwarden_api_api::models::ProjectUpdateRequestModel; +use bitwarden_crypto::KeyEncryptable; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; use uuid::Uuid; @@ -6,7 +7,6 @@ use uuid::Uuid; use super::ProjectResponse; use crate::{ client::Client, - crypto::KeyEncryptable, error::{Error, Result}, }; diff --git a/crates/bitwarden/src/secrets_manager/secrets/create.rs b/crates/bitwarden/src/secrets_manager/secrets/create.rs index a1bb81799..4f84223dc 100644 --- a/crates/bitwarden/src/secrets_manager/secrets/create.rs +++ b/crates/bitwarden/src/secrets_manager/secrets/create.rs @@ -1,11 +1,11 @@ use bitwarden_api_api::models::SecretCreateRequestModel; +use bitwarden_crypto::KeyEncryptable; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; use uuid::Uuid; use super::SecretResponse; use crate::{ - crypto::KeyEncryptable, error::{Error, Result}, Client, }; diff --git a/crates/bitwarden/src/secrets_manager/secrets/list.rs b/crates/bitwarden/src/secrets_manager/secrets/list.rs index 1f4ccba3f..7fa46d164 100644 --- a/crates/bitwarden/src/secrets_manager/secrets/list.rs +++ b/crates/bitwarden/src/secrets_manager/secrets/list.rs @@ -1,13 +1,13 @@ use bitwarden_api_api::models::{ SecretWithProjectsListResponseModel, SecretsWithProjectsInnerSecret, }; +use bitwarden_crypto::{Decryptable, EncString}; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; use uuid::Uuid; use crate::{ client::{encryption_settings::EncryptionSettings, Client}, - crypto::{Decryptable, EncString}, error::{Error, Result}, }; diff --git a/crates/bitwarden/src/secrets_manager/secrets/secret_response.rs b/crates/bitwarden/src/secrets_manager/secrets/secret_response.rs index a7fe49200..fe1a4d342 100644 --- a/crates/bitwarden/src/secrets_manager/secrets/secret_response.rs +++ b/crates/bitwarden/src/secrets_manager/secrets/secret_response.rs @@ -1,6 +1,7 @@ use bitwarden_api_api::models::{ BaseSecretResponseModel, BaseSecretResponseModelListResponseModel, SecretResponseModel, }; +use bitwarden_crypto::{Decryptable, EncString}; use chrono::{DateTime, Utc}; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; @@ -8,7 +9,6 @@ use uuid::Uuid; use crate::{ client::encryption_settings::EncryptionSettings, - crypto::{Decryptable, EncString}, error::{Error, Result}, }; diff --git a/crates/bitwarden/src/secrets_manager/secrets/update.rs b/crates/bitwarden/src/secrets_manager/secrets/update.rs index 970812c5a..f9e54f810 100644 --- a/crates/bitwarden/src/secrets_manager/secrets/update.rs +++ b/crates/bitwarden/src/secrets_manager/secrets/update.rs @@ -1,4 +1,5 @@ use bitwarden_api_api::models::SecretUpdateRequestModel; +use bitwarden_crypto::KeyEncryptable; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; use uuid::Uuid; @@ -6,7 +7,6 @@ use uuid::Uuid; use super::SecretResponse; use crate::{ client::Client, - crypto::KeyEncryptable, error::{Error, Result}, }; diff --git a/crates/bitwarden/src/secrets_manager/state.rs b/crates/bitwarden/src/secrets_manager/state.rs index d39603d34..4efa4403b 100644 --- a/crates/bitwarden/src/secrets_manager/state.rs +++ b/crates/bitwarden/src/secrets_manager/state.rs @@ -1,11 +1,12 @@ +use std::{fmt::Debug, path::Path}; + +use bitwarden_crypto::{EncString, KeyDecryptable, KeyEncryptable}; use serde::{Deserialize, Serialize}; use crate::{ client::AccessToken, - crypto::{EncString, KeyDecryptable, KeyEncryptable}, error::{Error, Result}, }; -use std::{fmt::Debug, path::Path}; const STATE_VERSION: u32 = 1; diff --git a/crates/bitwarden/src/tool/generators/client_generator.rs b/crates/bitwarden/src/tool/client_generator.rs similarity index 77% rename from crates/bitwarden/src/tool/generators/client_generator.rs rename to crates/bitwarden/src/tool/client_generator.rs index 301383635..16c786f9b 100644 --- a/crates/bitwarden/src/tool/generators/client_generator.rs +++ b/crates/bitwarden/src/tool/client_generator.rs @@ -1,10 +1,8 @@ +use bitwarden_generators::{passphrase, password, username}; + use crate::{ error::Result, - tool::generators::{ - passphrase::{passphrase, PassphraseGeneratorRequest}, - password::{password, PasswordGeneratorRequest}, - username::{username, UsernameGeneratorRequest}, - }, + generators::{PassphraseGeneratorRequest, PasswordGeneratorRequest, UsernameGeneratorRequest}, Client, }; @@ -20,7 +18,7 @@ impl<'a> ClientGenerator<'a> { /// # Examples /// /// ``` - /// use bitwarden::{Client, tool::PasswordGeneratorRequest, error::Result}; + /// use bitwarden::{Client, generators::PasswordGeneratorRequest, error::Result}; /// async fn test() -> Result<()> { /// let input = PasswordGeneratorRequest { /// lowercase: true, @@ -35,7 +33,7 @@ impl<'a> ClientGenerator<'a> { /// } /// ``` pub async fn password(&self, input: PasswordGeneratorRequest) -> Result { - password(input) + Ok(password(input)?) } /// Generates a random passphrase. @@ -48,7 +46,7 @@ impl<'a> ClientGenerator<'a> { /// # Examples /// /// ``` - /// use bitwarden::{Client, tool::PassphraseGeneratorRequest, error::Result}; + /// use bitwarden::{Client, generators::PassphraseGeneratorRequest, error::Result}; /// async fn test() -> Result<()> { /// let input = PassphraseGeneratorRequest { /// num_words: 4, @@ -60,17 +58,18 @@ impl<'a> ClientGenerator<'a> { /// } /// ``` pub async fn passphrase(&self, input: PassphraseGeneratorRequest) -> Result { - passphrase(input) + Ok(passphrase(input)?) } /// Generates a random username. - /// There are different username generation strategies, which can be customized using the `input` parameter. + /// There are different username generation strategies, which can be customized using the + /// `input` parameter. /// - /// Note that most generation strategies will be executed on the client side, but `Forwarded` will use third-party - /// services, which may require a specific setup or API key. + /// Note that most generation strategies will be executed on the client side, but `Forwarded` + /// will use third-party services, which may require a specific setup or API key. /// /// ``` - /// use bitwarden::{Client, tool::{UsernameGeneratorRequest}, error::Result}; + /// use bitwarden::{Client, generators::{UsernameGeneratorRequest}, error::Result}; /// async fn test() -> Result<()> { /// let input = UsernameGeneratorRequest::Word { /// capitalize: true, @@ -82,7 +81,7 @@ impl<'a> ClientGenerator<'a> { /// } /// ``` pub async fn username(&self, input: UsernameGeneratorRequest) -> Result { - username(input, self.client.get_http_client()).await + Ok(username(input, self.client.get_http_client()).await?) } } diff --git a/crates/bitwarden/src/tool/generators/mod.rs b/crates/bitwarden/src/tool/generators/mod.rs deleted file mode 100644 index 7966c58a9..000000000 --- a/crates/bitwarden/src/tool/generators/mod.rs +++ /dev/null @@ -1,10 +0,0 @@ -mod client_generator; -mod passphrase; -mod password; -mod username; -mod username_forwarders; - -pub use client_generator::ClientGenerator; -pub use passphrase::PassphraseGeneratorRequest; -pub use password::PasswordGeneratorRequest; -pub use username::{AppendType, ForwarderServiceType, UsernameGeneratorRequest}; diff --git a/crates/bitwarden/src/tool/generators/username_forwarders/addyio.rs b/crates/bitwarden/src/tool/generators/username_forwarders/addyio.rs deleted file mode 100644 index 0fc5937f6..000000000 --- a/crates/bitwarden/src/tool/generators/username_forwarders/addyio.rs +++ /dev/null @@ -1,143 +0,0 @@ -use reqwest::{header::CONTENT_TYPE, StatusCode}; - -use crate::error::Result; -pub async fn generate( - http: &reqwest::Client, - api_token: String, - domain: String, - base_url: String, - website: Option, -) -> Result { - let description = super::format_description(&website); - - #[derive(serde::Serialize)] - struct Request { - domain: String, - description: String, - } - - let response = http - .post(format!("{base_url}/api/v1/aliases")) - .header(CONTENT_TYPE, "application/json") - .bearer_auth(api_token) - .header("X-Requested-With", "XMLHttpRequest") - .json(&Request { - domain, - description, - }) - .send() - .await?; - - if response.status() == StatusCode::UNAUTHORIZED { - return Err("Invalid addy.io API token.".into()); - } - - // Throw any other errors - response.error_for_status_ref()?; - - #[derive(serde::Deserialize)] - struct ResponseData { - email: String, - } - #[derive(serde::Deserialize)] - struct Response { - data: ResponseData, - } - let response: Response = response.json().await?; - - Ok(response.data.email) -} - -#[cfg(test)] -mod tests { - use serde_json::json; - #[tokio::test] - async fn test_mock_server() { - use wiremock::{matchers, Mock, ResponseTemplate}; - - let (server, _client) = crate::util::start_mock(vec![ - // Mock the request to the addy.io API, and verify that the correct request is made - Mock::given(matchers::path("/api/v1/aliases")) - .and(matchers::method("POST")) - .and(matchers::header("Content-Type", "application/json")) - .and(matchers::header("Authorization", "Bearer MY_TOKEN")) - .and(matchers::body_json(json!({ - "domain": "myemail.com", - "description": "Website: example.com. Generated by Bitwarden." - }))) - .respond_with(ResponseTemplate::new(201).set_body_json(json!({ - "data": { - "id": "50c9e585-e7f5-41c4-9016-9014c15454bc", - "user_id": "ca0a4e09-c266-4f6f-845c-958db5090f09", - "local_part": "50c9e585-e7f5-41c4-9016-9014c15454bc", - "domain": "myemail.com", - "email": "50c9e585-e7f5-41c4-9016-9014c15454bc@myemail.com", - "active": true - } - }))) - .expect(1), - // Mock an invalid API token request - Mock::given(matchers::path("/api/v1/aliases")) - .and(matchers::method("POST")) - .and(matchers::header("Content-Type", "application/json")) - .and(matchers::header("Authorization", "Bearer MY_FAKE_TOKEN")) - .and(matchers::body_json(json!({ - "domain": "myemail.com", - "description": "Website: example.com. Generated by Bitwarden." - }))) - .respond_with(ResponseTemplate::new(401)) - .expect(1), - // Mock an invalid domain - Mock::given(matchers::path("/api/v1/aliases")) - .and(matchers::method("POST")) - .and(matchers::header("Content-Type", "application/json")) - .and(matchers::header("Authorization", "Bearer MY_TOKEN")) - .and(matchers::body_json(json!({ - "domain": "gmail.com", - "description": "Website: example.com. Generated by Bitwarden." - }))) - .respond_with(ResponseTemplate::new(403)) - .expect(1), - ]) - .await; - - let address = super::generate( - &reqwest::Client::new(), - "MY_TOKEN".into(), - "myemail.com".into(), - format!("http://{}", server.address()), - Some("example.com".into()), - ) - .await - .unwrap(); - - let fake_token_error = super::generate( - &reqwest::Client::new(), - "MY_FAKE_TOKEN".into(), - "myemail.com".into(), - format!("http://{}", server.address()), - Some("example.com".into()), - ) - .await - .unwrap_err(); - - assert!(fake_token_error - .to_string() - .contains("Invalid addy.io API token.")); - - let fake_domain_error = super::generate( - &reqwest::Client::new(), - "MY_TOKEN".into(), - "gmail.com".into(), - format!("http://{}", server.address()), - Some("example.com".into()), - ) - .await - .unwrap_err(); - - assert!(fake_domain_error.to_string().contains("403 Forbidden")); - - server.verify().await; - assert_eq!(address, "50c9e585-e7f5-41c4-9016-9014c15454bc@myemail.com"); - } -} diff --git a/crates/bitwarden/src/tool/generators/username_forwarders/forwardemail.rs b/crates/bitwarden/src/tool/generators/username_forwarders/forwardemail.rs deleted file mode 100644 index f4ba6ced6..000000000 --- a/crates/bitwarden/src/tool/generators/username_forwarders/forwardemail.rs +++ /dev/null @@ -1,193 +0,0 @@ -use reqwest::{header::CONTENT_TYPE, StatusCode}; - -use crate::error::{Error, Result}; - -pub async fn generate( - http: &reqwest::Client, - api_token: String, - domain: String, - website: Option, -) -> Result { - generate_with_api_url( - http, - api_token, - domain, - website, - "https://api.forwardemail.net".into(), - ) - .await -} - -async fn generate_with_api_url( - http: &reqwest::Client, - api_token: String, - domain: String, - website: Option, - api_url: String, -) -> Result { - let description = super::format_description(&website); - - #[derive(serde::Serialize)] - struct Request { - labels: Option, - description: String, - } - - let response = http - .post(format!("{api_url}/v1/domains/{domain}/aliases")) - .header(CONTENT_TYPE, "application/json") - .basic_auth(api_token, None::) - .json(&Request { - description, - labels: website, - }) - .send() - .await?; - - if response.status() == StatusCode::UNAUTHORIZED { - return Err("Invalid Forward Email API key.".into()); - } - - #[derive(serde::Deserialize)] - struct ResponseDomain { - name: Option, - } - #[derive(serde::Deserialize)] - struct Response { - name: Option, - domain: Option, - - message: Option, - error: Option, - } - let status = response.status(); - let response: Response = response.json().await?; - - if status.is_success() { - if let Some(name) = response.name { - if let Some(response_domain) = response.domain { - return Ok(format!( - "{}@{}", - name, - response_domain.name.unwrap_or(domain) - )); - } - } - } - - if let Some(message) = response.message { - return Err(Error::ResponseContent { status, message }); - } - if let Some(message) = response.error { - return Err(Error::ResponseContent { status, message }); - } - - Err("Unknown ForwardEmail error.".into()) -} - -#[cfg(test)] -mod tests { - use serde_json::json; - - #[tokio::test] - async fn test_mock_server() { - use wiremock::{matchers, Mock, ResponseTemplate}; - - let (server, _client) = crate::util::start_mock(vec![ - // Mock the request to the ForwardEmail API, and verify that the correct request is made - Mock::given(matchers::path("/v1/domains/mydomain.com/aliases")) - .and(matchers::method("POST")) - .and(matchers::header("Content-Type", "application/json")) - .and(matchers::header("Authorization", "Basic TVlfVE9LRU46")) - .and(matchers::body_json(json!({ - "labels": "example.com", - "description": "Website: example.com. Generated by Bitwarden." - }))) - .respond_with(ResponseTemplate::new(201).set_body_json(json!({ - "name": "wertg8ad", - "domain": { - "name": "mydomain.com" - } - }))) - .expect(1), - // Mock an invalid API token request - Mock::given(matchers::path("/v1/domains/mydomain.com/aliases")) - .and(matchers::method("POST")) - .and(matchers::header("Content-Type", "application/json")) - .and(matchers::header( - "Authorization", - "Basic TVlfRkFLRV9UT0tFTjo=", - )) - .and(matchers::body_json(json!({ - "labels": "example.com", - "description": "Website: example.com. Generated by Bitwarden." - }))) - .respond_with(ResponseTemplate::new(401).set_body_json(json!({ - "statusCode": 401, - "error": "Unauthorized", - "message": "Invalid API token." - }))) - .expect(1), - // Mock a free API token request - Mock::given(matchers::path("/v1/domains/mydomain.com/aliases")) - .and(matchers::method("POST")) - .and(matchers::header("Content-Type", "application/json")) - .and(matchers::header( - "Authorization", - "Basic TVlfRlJFRV9UT0tFTjo=", - )) - .and(matchers::body_json(json!({ - "labels": "example.com", - "description": "Website: example.com. Generated by Bitwarden." - }))) - .respond_with(ResponseTemplate::new(402).set_body_json(json!({ - "statusCode": 402, - "error": "Payment required", - "message": "Please upgrade to a paid plan to unlock this feature." - }))) - .expect(1), - ]) - .await; - - let address = super::generate_with_api_url( - &reqwest::Client::new(), - "MY_TOKEN".into(), - "mydomain.com".into(), - Some("example.com".into()), - format!("http://{}", server.address()), - ) - .await - .unwrap(); - assert_eq!(address, "wertg8ad@mydomain.com"); - - let invalid_token_error = super::generate_with_api_url( - &reqwest::Client::new(), - "MY_FAKE_TOKEN".into(), - "mydomain.com".into(), - Some("example.com".into()), - format!("http://{}", server.address()), - ) - .await - .unwrap_err(); - - assert!(invalid_token_error - .to_string() - .contains("Invalid Forward Email API key.")); - - let free_token_error = super::generate_with_api_url( - &reqwest::Client::new(), - "MY_FREE_TOKEN".into(), - "mydomain.com".into(), - Some("example.com".into()), - format!("http://{}", server.address()), - ) - .await - .unwrap_err(); - - assert!(free_token_error - .to_string() - .contains("Please upgrade to a paid plan")); - - server.verify().await; - } -} diff --git a/crates/bitwarden/src/tool/generators/username_forwarders/simplelogin.rs b/crates/bitwarden/src/tool/generators/username_forwarders/simplelogin.rs deleted file mode 100644 index 6c4f9dab4..000000000 --- a/crates/bitwarden/src/tool/generators/username_forwarders/simplelogin.rs +++ /dev/null @@ -1,114 +0,0 @@ -use reqwest::{header::CONTENT_TYPE, StatusCode}; - -use crate::error::Result; - -pub async fn generate( - http: &reqwest::Client, - api_key: String, - website: Option, -) -> Result { - generate_with_api_url(http, api_key, website, "https://app.simplelogin.io".into()).await -} - -async fn generate_with_api_url( - http: &reqwest::Client, - api_key: String, - website: Option, - api_url: String, -) -> Result { - let query = website - .as_ref() - .map(|w| format!("?hostname={}", w)) - .unwrap_or_default(); - - let note = super::format_description(&website); - - #[derive(serde::Serialize)] - struct Request { - note: String, - } - - let response = http - .post(format!("{api_url}/api/alias/random/new{query}")) - .header(CONTENT_TYPE, "application/json") - .header("Authentication", api_key) - .json(&Request { note }) - .send() - .await?; - - if response.status() == StatusCode::UNAUTHORIZED { - return Err("Invalid SimpleLogin API key.".into()); - } - - // Throw any other errors - response.error_for_status_ref()?; - - #[derive(serde::Deserialize)] - struct Response { - alias: String, - } - let response: Response = response.json().await?; - - Ok(response.alias) -} - -#[cfg(test)] -mod tests { - use serde_json::json; - #[tokio::test] - async fn test_mock_server() { - use wiremock::{matchers, Mock, ResponseTemplate}; - - let (server, _client) = crate::util::start_mock(vec![ - // Mock the request to the SimpleLogin API, and verify that the correct request is made - Mock::given(matchers::path("/api/alias/random/new")) - .and(matchers::method("POST")) - .and(matchers::query_param("hostname", "example.com")) - .and(matchers::header("Content-Type", "application/json")) - .and(matchers::header("Authentication", "MY_TOKEN")) - .and(matchers::body_json(json!({ - "note": "Website: example.com. Generated by Bitwarden." - }))) - .respond_with(ResponseTemplate::new(201).set_body_json(json!({ - "alias": "simplelogin.yut3g8@aleeas.com", - }))) - .expect(1), - // Mock an invalid token request - Mock::given(matchers::path("/api/alias/random/new")) - .and(matchers::method("POST")) - .and(matchers::query_param("hostname", "example.com")) - .and(matchers::header("Content-Type", "application/json")) - .and(matchers::header("Authentication", "MY_FAKE_TOKEN")) - .and(matchers::body_json(json!({ - "note": "Website: example.com. Generated by Bitwarden." - }))) - .respond_with(ResponseTemplate::new(401)) - .expect(1), - ]) - .await; - - let address = super::generate_with_api_url( - &reqwest::Client::new(), - "MY_TOKEN".into(), - Some("example.com".into()), - format!("http://{}", server.address()), - ) - .await - .unwrap(); - assert_eq!(address, "simplelogin.yut3g8@aleeas.com"); - - let fake_token_error = super::generate_with_api_url( - &reqwest::Client::new(), - "MY_FAKE_TOKEN".into(), - Some("example.com".into()), - format!("http://{}", server.address()), - ) - .await - .unwrap_err(); - assert!(fake_token_error - .to_string() - .contains("Invalid SimpleLogin API key.")); - - server.verify().await; - } -} diff --git a/crates/bitwarden/src/tool/mod.rs b/crates/bitwarden/src/tool/mod.rs index fe41b68db..a94528163 100644 --- a/crates/bitwarden/src/tool/mod.rs +++ b/crates/bitwarden/src/tool/mod.rs @@ -1,8 +1,4 @@ mod exporters; -mod generators; - pub use exporters::{ClientExporters, ExportFormat}; -pub use generators::{ - AppendType, ClientGenerator, ForwarderServiceType, PassphraseGeneratorRequest, - PasswordGeneratorRequest, UsernameGeneratorRequest, -}; +mod client_generator; +pub use client_generator::ClientGenerator; diff --git a/crates/bitwarden/src/uniffi_support.rs b/crates/bitwarden/src/uniffi_support.rs index 795aa9f50..7da53c8b5 100644 --- a/crates/bitwarden/src/uniffi_support.rs +++ b/crates/bitwarden/src/uniffi_support.rs @@ -1,44 +1,16 @@ use std::{num::NonZeroU32, str::FromStr}; +use bitwarden_crypto::{AsymmetricEncString, EncString}; use uuid::Uuid; -use crate::{ - crypto::{AsymmEncString, EncString}, - error::Error, - UniffiCustomTypeConverter, -}; +use crate::UniffiCustomTypeConverter; -uniffi::custom_type!(NonZeroU32, u32); +uniffi::ffi_converter_forward!(NonZeroU32, bitwarden_crypto::UniFfiTag, crate::UniFfiTag); +uniffi::ffi_converter_forward!(EncString, bitwarden_crypto::UniFfiTag, crate::UniFfiTag); -impl UniffiCustomTypeConverter for NonZeroU32 { - type Builtin = u32; +uniffi::custom_type!(AsymmetricEncString, String); - fn into_custom(val: Self::Builtin) -> uniffi::Result { - Self::new(val).ok_or(Error::from("Number is zero").into()) - } - - fn from_custom(obj: Self) -> Self::Builtin { - obj.get() - } -} - -uniffi::custom_type!(EncString, String); - -impl UniffiCustomTypeConverter for EncString { - type Builtin = String; - - fn into_custom(val: Self::Builtin) -> uniffi::Result { - Self::from_str(&val).map_err(|e| e.into()) - } - - fn from_custom(obj: Self) -> Self::Builtin { - obj.to_string() - } -} - -uniffi::custom_type!(AsymmEncString, String); - -impl UniffiCustomTypeConverter for AsymmEncString { +impl UniffiCustomTypeConverter for AsymmetricEncString { type Builtin = String; fn into_custom(val: Self::Builtin) -> uniffi::Result { diff --git a/crates/bitwarden/src/util.rs b/crates/bitwarden/src/util.rs index 038c3d7ee..f6568986d 100644 --- a/crates/bitwarden/src/util.rs +++ b/crates/bitwarden/src/util.rs @@ -27,18 +27,6 @@ const INDIFFERENT: GeneralPurposeConfig = pub const STANDARD_INDIFFERENT: GeneralPurpose = GeneralPurpose::new(&alphabet::STANDARD, INDIFFERENT); -#[cfg(feature = "mobile")] -pub(crate) 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)] pub async fn start_mock(mocks: Vec) -> (wiremock::MockServer, crate::Client) { let server = wiremock::MockServer::start().await; diff --git a/crates/bitwarden/src/vault/cipher/attachment.rs b/crates/bitwarden/src/vault/cipher/attachment.rs index e2d11f592..1ec6be6fe 100644 --- a/crates/bitwarden/src/vault/cipher/attachment.rs +++ b/crates/bitwarden/src/vault/cipher/attachment.rs @@ -1,10 +1,11 @@ +use bitwarden_crypto::{ + CryptoError, EncString, KeyDecryptable, KeyEncryptable, SymmetricCryptoKey, +}; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; -use crate::{ - crypto::{EncString, KeyDecryptable, KeyEncryptable, SymmetricCryptoKey}, - error::{Error, Result}, -}; +use super::Cipher; +use crate::error::{Error, Result}; #[derive(Serialize, Deserialize, Debug, JsonSchema)] #[serde(rename_all = "camelCase", deny_unknown_fields)] @@ -31,8 +32,68 @@ pub struct AttachmentView { pub key: Option, } -impl KeyEncryptable for AttachmentView { - fn encrypt_with_key(self, key: &SymmetricCryptoKey) -> Result { +#[derive(Serialize, Deserialize, Debug, JsonSchema)] +#[serde(rename_all = "camelCase", deny_unknown_fields)] +#[cfg_attr(feature = "mobile", derive(uniffi::Record))] +pub struct AttachmentEncryptResult { + pub attachment: Attachment, + pub contents: Vec, +} + +pub struct AttachmentFile { + pub cipher: Cipher, + pub attachment: Attachment, + pub contents: EncString, +} + +pub struct AttachmentFileView<'a> { + pub cipher: Cipher, + pub attachment: AttachmentView, + pub contents: &'a [u8], +} + +impl<'a> KeyEncryptable for AttachmentFileView<'a> { + fn encrypt_with_key( + self, + key: &SymmetricCryptoKey, + ) -> Result { + let ciphers_key = Cipher::get_cipher_key(key, &self.cipher.key)?; + let ciphers_key = ciphers_key.as_ref().unwrap_or(key); + + let mut attachment = self.attachment; + + // Because this is a new attachment, we have to generate a key for it, encrypt the contents + // with it, and then encrypt the key with the cipher key + let attachment_key = SymmetricCryptoKey::generate(rand::thread_rng()); + let encrypted_contents = self.contents.encrypt_with_key(&attachment_key)?; + attachment.key = Some(attachment_key.to_vec().encrypt_with_key(ciphers_key)?); + + Ok(AttachmentEncryptResult { + attachment: attachment.encrypt_with_key(ciphers_key)?, + contents: encrypted_contents.to_buffer()?, + }) + } +} + +impl KeyDecryptable> for AttachmentFile { + fn decrypt_with_key(&self, key: &SymmetricCryptoKey) -> Result, CryptoError> { + let ciphers_key = Cipher::get_cipher_key(key, &self.cipher.key)?; + let ciphers_key = ciphers_key.as_ref().unwrap_or(key); + + let mut attachment_key: Vec = self + .attachment + .key + .as_ref() + .ok_or(CryptoError::MissingKey)? + .decrypt_with_key(ciphers_key)?; + let attachment_key = SymmetricCryptoKey::try_from(attachment_key.as_mut_slice())?; + + self.contents.decrypt_with_key(&attachment_key) + } +} + +impl KeyEncryptable for AttachmentView { + fn encrypt_with_key(self, key: &SymmetricCryptoKey) -> Result { Ok(Attachment { id: self.id, url: self.url, @@ -44,8 +105,8 @@ impl KeyEncryptable for AttachmentView { } } -impl KeyDecryptable for Attachment { - fn decrypt_with_key(&self, key: &SymmetricCryptoKey) -> Result { +impl KeyDecryptable for Attachment { + fn decrypt_with_key(&self, key: &SymmetricCryptoKey) -> Result { Ok(AttachmentView { id: self.id.clone(), url: self.url.clone(), @@ -71,3 +132,68 @@ impl TryFrom for Attachment }) } } + +#[cfg(test)] +mod tests { + use base64::{engine::general_purpose::STANDARD, Engine}; + use bitwarden_crypto::{EncString, KeyDecryptable, SymmetricCryptoKey}; + + use crate::vault::{ + cipher::cipher::{CipherRepromptType, CipherType}, + Attachment, AttachmentFile, Cipher, + }; + + #[test] + fn test_attachment_key() { + let user_key : SymmetricCryptoKey = "w2LO+nwV4oxwswVYCxlOfRUseXfvU03VzvKQHrqeklPgiMZrspUe6sOBToCnDn9Ay0tuCBn8ykVVRb7PWhub2Q==".parse().unwrap(); + + let attachment = Attachment { + id: None, + url: None, + size: Some("161".into()), + size_name: Some("161 Bytes".into()), + file_name: Some("2.M3z1MOO9eBG9BWRTEUbPog==|jPw0By1AakHDfoaY8UOwOQ==|eP9/J1583OJpHsSM4ZnXZzdBHfqVTXnOXGlkkmAKSfA=".parse().unwrap()), + key: Some("2.r288/AOSPiaLFkW07EBGBw==|SAmnnCbOLFjX5lnURvoualOetQwuyPc54PAmHDTRrhT0gwO9ailna9U09q9bmBfI5XrjNNEsuXssgzNygRkezoVQvZQggZddOwHB6KQW5EQ=|erIMUJp8j+aTcmhdE50zEX+ipv/eR1sZ7EwULJm/6DY=".parse().unwrap()) + }; + + let cipher = Cipher { + id: None, + organization_id: None, + folder_id: None, + collection_ids: Vec::new(), + key: Some("2.Gg8yCM4IIgykCZyq0O4+cA==|GJLBtfvSJTDJh/F7X4cJPkzI6ccnzJm5DYl3yxOW2iUn7DgkkmzoOe61sUhC5dgVdV0kFqsZPcQ0yehlN1DDsFIFtrb4x7LwzJNIkMgxNyg=|1rGkGJ8zcM5o5D0aIIwAyLsjMLrPsP3EWm3CctBO3Fw=".parse().unwrap()), + name: "2.d24xECyEdMZ3MG9s6SrGNw==|XvJlTeu5KJ22M3jKosy6iw==|8xGiQty4X61cDMx6PVqkJfSQ0ZTdA/5L9TpG7QfovoM=".parse().unwrap(), + notes: None, + r#type: CipherType::Login, + login: None, + identity: None, + card: None, + secure_note: None, + favorite: false, + reprompt: CipherRepromptType::None, + organization_use_totp: false, + edit: true, + view_password: true, + local_data: None, + attachments: None, + fields: None, + password_history: None, + creation_date: "2023-07-24T12:05:09.466666700Z".parse().unwrap(), + deleted_date: None, + revision_date: "2023-07-27T19:28:05.240Z".parse().unwrap(), + }; + + let enc_file = STANDARD.decode(b"Ao00qr1xLsV+ZNQpYZ/UwEwOWo3hheKwCYcOGIbsorZ6JIG2vLWfWEXCVqP0hDuzRvmx8otApNZr8pJYLNwCe1aQ+ySHQYGkdubFjoMojulMbQ959Y4SJ6Its/EnVvpbDnxpXTDpbutDxyhxfq1P3lstL2G9rObJRrxiwdGlRGu1h94UA1fCCkIUQux5LcqUee6W4MyQmRnsUziH8gGzmtI=").unwrap(); + let original = STANDARD.decode(b"rMweTemxOL9D0iWWfRxiY3enxiZ5IrwWD6ef2apGO6MvgdGhy2fpwmATmn7BpSj9lRumddLLXm7u8zSp6hnXt1hS71YDNh78LjGKGhGL4sbg8uNnpa/I6GK/83jzqGYN7+ESbg==").unwrap(); + + let dec = AttachmentFile { + cipher, + attachment, + contents: EncString::from_buffer(&enc_file).unwrap(), + } + .decrypt_with_key(&user_key) + .unwrap(); + + assert_eq!(dec, original); + } +} diff --git a/crates/bitwarden/src/vault/cipher/card.rs b/crates/bitwarden/src/vault/cipher/card.rs index 6f96041e9..cd61a17d8 100644 --- a/crates/bitwarden/src/vault/cipher/card.rs +++ b/crates/bitwarden/src/vault/cipher/card.rs @@ -1,11 +1,11 @@ use bitwarden_api_api::models::CipherCardModel; +use bitwarden_crypto::{ + CryptoError, EncString, KeyDecryptable, KeyEncryptable, SymmetricCryptoKey, +}; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; -use crate::{ - crypto::{EncString, KeyDecryptable, KeyEncryptable, SymmetricCryptoKey}, - error::{Error, Result}, -}; +use crate::error::{Error, Result}; #[derive(Serialize, Deserialize, Debug, JsonSchema)] #[serde(rename_all = "camelCase", deny_unknown_fields)] @@ -31,8 +31,8 @@ pub struct CardView { pub number: Option, } -impl KeyEncryptable for CardView { - fn encrypt_with_key(self, key: &SymmetricCryptoKey) -> Result { +impl KeyEncryptable for CardView { + fn encrypt_with_key(self, key: &SymmetricCryptoKey) -> Result { Ok(Card { cardholder_name: self.cardholder_name.encrypt_with_key(key)?, exp_month: self.exp_month.encrypt_with_key(key)?, @@ -44,15 +44,15 @@ impl KeyEncryptable for CardView { } } -impl KeyDecryptable for Card { - fn decrypt_with_key(&self, key: &SymmetricCryptoKey) -> Result { +impl KeyDecryptable for Card { + fn decrypt_with_key(&self, key: &SymmetricCryptoKey) -> Result { Ok(CardView { - cardholder_name: self.cardholder_name.decrypt_with_key(key)?, - exp_month: self.exp_month.decrypt_with_key(key)?, - exp_year: self.exp_year.decrypt_with_key(key)?, - code: self.code.decrypt_with_key(key)?, - brand: self.brand.decrypt_with_key(key)?, - number: self.number.decrypt_with_key(key)?, + cardholder_name: self.cardholder_name.decrypt_with_key(key).ok().flatten(), + exp_month: self.exp_month.decrypt_with_key(key).ok().flatten(), + exp_year: self.exp_year.decrypt_with_key(key).ok().flatten(), + code: self.code.decrypt_with_key(key).ok().flatten(), + brand: self.brand.decrypt_with_key(key).ok().flatten(), + number: self.number.decrypt_with_key(key).ok().flatten(), }) } } diff --git a/crates/bitwarden/src/vault/cipher/cipher.rs b/crates/bitwarden/src/vault/cipher/cipher.rs index e44a3757a..bbd944871 100644 --- a/crates/bitwarden/src/vault/cipher/cipher.rs +++ b/crates/bitwarden/src/vault/cipher/cipher.rs @@ -1,4 +1,8 @@ use bitwarden_api_api::models::CipherDetailsResponseModel; +use bitwarden_crypto::{ + CryptoError, EncString, KeyContainer, KeyDecryptable, KeyEncryptable, LocateKey, + SymmetricCryptoKey, +}; use chrono::{DateTime, Utc}; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; @@ -11,8 +15,6 @@ use super::{ login, secure_note, }; use crate::{ - client::encryption_settings::EncryptionSettings, - crypto::{EncString, KeyDecryptable, KeyEncryptable, LocateKey, SymmetricCryptoKey}, error::{Error, Result}, vault::password_history, }; @@ -44,7 +46,8 @@ pub struct Cipher { pub folder_id: Option, pub collection_ids: Vec, - /// More recent ciphers uses individual encryption keys to encrypt the other fields of the Cipher. + /// More recent ciphers uses individual encryption keys to encrypt the other fields of the + /// Cipher. pub key: Option, pub name: EncString, @@ -135,8 +138,8 @@ pub struct CipherListView { pub revision_date: DateTime, } -impl KeyEncryptable for CipherView { - fn encrypt_with_key(self, key: &SymmetricCryptoKey) -> Result { +impl KeyEncryptable for CipherView { + fn encrypt_with_key(self, key: &SymmetricCryptoKey) -> Result { let ciphers_key = Cipher::get_cipher_key(key, &self.key)?; let key = ciphers_key.as_ref().unwrap_or(key); @@ -169,8 +172,8 @@ impl KeyEncryptable for CipherView { } } -impl KeyDecryptable for Cipher { - fn decrypt_with_key(&self, key: &SymmetricCryptoKey) -> Result { +impl KeyDecryptable for Cipher { + fn decrypt_with_key(&self, key: &SymmetricCryptoKey) -> Result { let ciphers_key = Cipher::get_cipher_key(key, &self.key)?; let key = ciphers_key.as_ref().unwrap_or(key); @@ -180,22 +183,22 @@ impl KeyDecryptable for Cipher { folder_id: self.folder_id, collection_ids: self.collection_ids.clone(), key: self.key.clone(), - name: self.name.decrypt_with_key(key)?, - notes: self.notes.decrypt_with_key(key)?, + name: self.name.decrypt_with_key(key).ok().unwrap_or_default(), + notes: self.notes.decrypt_with_key(key).ok().flatten(), r#type: self.r#type, - login: self.login.decrypt_with_key(key)?, - identity: self.identity.decrypt_with_key(key)?, - card: self.card.decrypt_with_key(key)?, - secure_note: self.secure_note.decrypt_with_key(key)?, + login: self.login.decrypt_with_key(key).ok().flatten(), + identity: self.identity.decrypt_with_key(key).ok().flatten(), + card: self.card.decrypt_with_key(key).ok().flatten(), + secure_note: self.secure_note.decrypt_with_key(key).ok().flatten(), favorite: self.favorite, reprompt: self.reprompt, organization_use_totp: self.organization_use_totp, edit: self.edit, view_password: self.view_password, - local_data: self.local_data.decrypt_with_key(key)?, - attachments: self.attachments.decrypt_with_key(key)?, - fields: self.fields.decrypt_with_key(key)?, - password_history: self.password_history.decrypt_with_key(key)?, + local_data: self.local_data.decrypt_with_key(key).ok().flatten(), + attachments: self.attachments.decrypt_with_key(key).ok().flatten(), + fields: self.fields.decrypt_with_key(key).ok().flatten(), + password_history: self.password_history.decrypt_with_key(key).ok().flatten(), creation_date: self.creation_date, deleted_date: self.deleted_date, revision_date: self.revision_date, @@ -208,20 +211,20 @@ impl Cipher { /// Note that some ciphers do not have individual encryption keys, /// in which case this will return Ok(None) and the key associated /// with this cipher's user or organization must be used instead - fn get_cipher_key( + pub(super) fn get_cipher_key( key: &SymmetricCryptoKey, ciphers_key: &Option, - ) -> Result> { + ) -> Result, CryptoError> { ciphers_key .as_ref() .map(|k| { - let key: Vec = k.decrypt_with_key(key)?; - SymmetricCryptoKey::try_from(key.as_slice()) + let mut key: Vec = k.decrypt_with_key(key)?; + SymmetricCryptoKey::try_from(key.as_mut_slice()) }) .transpose() } - fn get_decrypted_subtitle(&self, key: &SymmetricCryptoKey) -> Result { + fn get_decrypted_subtitle(&self, key: &SymmetricCryptoKey) -> Result { Ok(match self.r#type { CipherType::Login => { let Some(login) = &self.login else { @@ -286,8 +289,8 @@ impl Cipher { } } -impl KeyDecryptable for Cipher { - fn decrypt_with_key(&self, key: &SymmetricCryptoKey) -> Result { +impl KeyDecryptable for Cipher { + fn decrypt_with_key(&self, key: &SymmetricCryptoKey) -> Result { let ciphers_key = Cipher::get_cipher_key(key, &self.key)?; let key = ciphers_key.as_ref().unwrap_or(key); @@ -296,8 +299,8 @@ impl KeyDecryptable for Cipher { organization_id: self.organization_id, folder_id: self.folder_id, collection_ids: self.collection_ids.clone(), - name: self.name.decrypt_with_key(key)?, - sub_title: self.get_decrypted_subtitle(key)?, + name: self.name.decrypt_with_key(key).ok().unwrap_or_default(), + sub_title: self.get_decrypted_subtitle(key).ok().unwrap_or_default(), r#type: self.r#type, favorite: self.favorite, reprompt: self.reprompt, @@ -318,7 +321,7 @@ impl KeyDecryptable for Cipher { impl LocateKey for Cipher { fn locate_key<'a>( &self, - enc: &'a EncryptionSettings, + enc: &'a dyn KeyContainer, _: &Option, ) -> Option<&'a SymmetricCryptoKey> { enc.get_key(&self.organization_id) @@ -327,7 +330,7 @@ impl LocateKey for Cipher { impl LocateKey for CipherView { fn locate_key<'a>( &self, - enc: &'a EncryptionSettings, + enc: &'a dyn KeyContainer, _: &Option, ) -> Option<&'a SymmetricCryptoKey> { enc.get_key(&self.organization_id) diff --git a/crates/bitwarden/src/vault/cipher/field.rs b/crates/bitwarden/src/vault/cipher/field.rs index dee71872b..db896474f 100644 --- a/crates/bitwarden/src/vault/cipher/field.rs +++ b/crates/bitwarden/src/vault/cipher/field.rs @@ -1,13 +1,13 @@ use bitwarden_api_api::models::CipherFieldModel; +use bitwarden_crypto::{ + CryptoError, EncString, KeyDecryptable, KeyEncryptable, SymmetricCryptoKey, +}; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; use serde_repr::{Deserialize_repr, Serialize_repr}; use super::linked_id::LinkedIdType; -use crate::{ - crypto::{EncString, KeyDecryptable, KeyEncryptable, SymmetricCryptoKey}, - error::{Error, Result}, -}; +use crate::error::{Error, Result}; #[derive(Clone, Copy, Serialize_repr, Deserialize_repr, Debug, JsonSchema)] #[repr(u8)] @@ -41,8 +41,8 @@ pub struct FieldView { linked_id: Option, } -impl KeyEncryptable for FieldView { - fn encrypt_with_key(self, key: &SymmetricCryptoKey) -> Result { +impl KeyEncryptable for FieldView { + fn encrypt_with_key(self, key: &SymmetricCryptoKey) -> Result { Ok(Field { name: self.name.encrypt_with_key(key)?, value: self.value.encrypt_with_key(key)?, @@ -52,11 +52,11 @@ impl KeyEncryptable for FieldView { } } -impl KeyDecryptable for Field { - fn decrypt_with_key(&self, key: &SymmetricCryptoKey) -> Result { +impl KeyDecryptable for Field { + fn decrypt_with_key(&self, key: &SymmetricCryptoKey) -> Result { Ok(FieldView { - name: self.name.decrypt_with_key(key)?, - value: self.value.decrypt_with_key(key)?, + name: self.name.decrypt_with_key(key).ok().flatten(), + value: self.value.decrypt_with_key(key).ok().flatten(), r#type: self.r#type, linked_id: self.linked_id, }) diff --git a/crates/bitwarden/src/vault/cipher/identity.rs b/crates/bitwarden/src/vault/cipher/identity.rs index 922d2aee6..f59166eec 100644 --- a/crates/bitwarden/src/vault/cipher/identity.rs +++ b/crates/bitwarden/src/vault/cipher/identity.rs @@ -1,11 +1,11 @@ use bitwarden_api_api::models::CipherIdentityModel; +use bitwarden_crypto::{ + CryptoError, EncString, KeyDecryptable, KeyEncryptable, SymmetricCryptoKey, +}; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; -use crate::{ - crypto::{EncString, KeyDecryptable, KeyEncryptable, SymmetricCryptoKey}, - error::{Error, Result}, -}; +use crate::error::{Error, Result}; #[derive(Serialize, Deserialize, Debug, JsonSchema)] #[serde(rename_all = "camelCase", deny_unknown_fields)] @@ -55,8 +55,8 @@ pub struct IdentityView { pub license_number: Option, } -impl KeyEncryptable for IdentityView { - fn encrypt_with_key(self, key: &SymmetricCryptoKey) -> Result { +impl KeyEncryptable for IdentityView { + fn encrypt_with_key(self, key: &SymmetricCryptoKey) -> Result { Ok(Identity { title: self.title.encrypt_with_key(key)?, first_name: self.first_name.encrypt_with_key(key)?, @@ -80,27 +80,27 @@ impl KeyEncryptable for IdentityView { } } -impl KeyDecryptable for Identity { - fn decrypt_with_key(&self, key: &SymmetricCryptoKey) -> Result { +impl KeyDecryptable for Identity { + fn decrypt_with_key(&self, key: &SymmetricCryptoKey) -> Result { Ok(IdentityView { - title: self.title.decrypt_with_key(key)?, - first_name: self.first_name.decrypt_with_key(key)?, - middle_name: self.middle_name.decrypt_with_key(key)?, - last_name: self.last_name.decrypt_with_key(key)?, - address1: self.address1.decrypt_with_key(key)?, - address2: self.address2.decrypt_with_key(key)?, - address3: self.address3.decrypt_with_key(key)?, - city: self.city.decrypt_with_key(key)?, - state: self.state.decrypt_with_key(key)?, - postal_code: self.postal_code.decrypt_with_key(key)?, - country: self.country.decrypt_with_key(key)?, - company: self.company.decrypt_with_key(key)?, - email: self.email.decrypt_with_key(key)?, - phone: self.phone.decrypt_with_key(key)?, - ssn: self.ssn.decrypt_with_key(key)?, - username: self.username.decrypt_with_key(key)?, - passport_number: self.passport_number.decrypt_with_key(key)?, - license_number: self.license_number.decrypt_with_key(key)?, + title: self.title.decrypt_with_key(key).ok().flatten(), + first_name: self.first_name.decrypt_with_key(key).ok().flatten(), + middle_name: self.middle_name.decrypt_with_key(key).ok().flatten(), + last_name: self.last_name.decrypt_with_key(key).ok().flatten(), + address1: self.address1.decrypt_with_key(key).ok().flatten(), + address2: self.address2.decrypt_with_key(key).ok().flatten(), + address3: self.address3.decrypt_with_key(key).ok().flatten(), + city: self.city.decrypt_with_key(key).ok().flatten(), + state: self.state.decrypt_with_key(key).ok().flatten(), + postal_code: self.postal_code.decrypt_with_key(key).ok().flatten(), + country: self.country.decrypt_with_key(key).ok().flatten(), + company: self.company.decrypt_with_key(key).ok().flatten(), + email: self.email.decrypt_with_key(key).ok().flatten(), + phone: self.phone.decrypt_with_key(key).ok().flatten(), + ssn: self.ssn.decrypt_with_key(key).ok().flatten(), + username: self.username.decrypt_with_key(key).ok().flatten(), + passport_number: self.passport_number.decrypt_with_key(key).ok().flatten(), + license_number: self.license_number.decrypt_with_key(key).ok().flatten(), }) } } diff --git a/crates/bitwarden/src/vault/cipher/local_data.rs b/crates/bitwarden/src/vault/cipher/local_data.rs index 8d5fe8694..6a85512c2 100644 --- a/crates/bitwarden/src/vault/cipher/local_data.rs +++ b/crates/bitwarden/src/vault/cipher/local_data.rs @@ -1,11 +1,7 @@ +use bitwarden_crypto::{CryptoError, KeyDecryptable, KeyEncryptable, SymmetricCryptoKey}; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; -use crate::{ - crypto::{KeyDecryptable, KeyEncryptable, SymmetricCryptoKey}, - error::Result, -}; - #[derive(Serialize, Deserialize, Debug, JsonSchema)] #[serde(rename_all = "camelCase", deny_unknown_fields)] #[cfg_attr(feature = "mobile", derive(uniffi::Record))] @@ -22,8 +18,8 @@ pub struct LocalDataView { last_launched: Option, } -impl KeyEncryptable for LocalDataView { - fn encrypt_with_key(self, _key: &SymmetricCryptoKey) -> Result { +impl KeyEncryptable for LocalDataView { + fn encrypt_with_key(self, _key: &SymmetricCryptoKey) -> Result { Ok(LocalData { last_used_date: self.last_used_date, last_launched: self.last_launched, @@ -31,8 +27,8 @@ impl KeyEncryptable for LocalDataView { } } -impl KeyDecryptable for LocalData { - fn decrypt_with_key(&self, _key: &SymmetricCryptoKey) -> Result { +impl KeyDecryptable for LocalData { + fn decrypt_with_key(&self, _key: &SymmetricCryptoKey) -> Result { Ok(LocalDataView { last_used_date: self.last_used_date, last_launched: self.last_launched, diff --git a/crates/bitwarden/src/vault/cipher/login.rs b/crates/bitwarden/src/vault/cipher/login.rs index da71f963f..ea5b1f357 100644 --- a/crates/bitwarden/src/vault/cipher/login.rs +++ b/crates/bitwarden/src/vault/cipher/login.rs @@ -1,13 +1,13 @@ use bitwarden_api_api::models::{CipherLoginModel, CipherLoginUriModel}; +use bitwarden_crypto::{ + CryptoError, EncString, KeyDecryptable, KeyEncryptable, SymmetricCryptoKey, +}; use chrono::{DateTime, Utc}; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; use serde_repr::{Deserialize_repr, Serialize_repr}; -use crate::{ - crypto::{EncString, KeyDecryptable, KeyEncryptable, SymmetricCryptoKey}, - error::{Error, Result}, -}; +use crate::error::{Error, Result}; #[derive(Clone, Copy, Serialize_repr, Deserialize_repr, Debug, JsonSchema)] #[repr(u8)] @@ -64,8 +64,8 @@ pub struct LoginView { pub autofill_on_page_load: Option, } -impl KeyEncryptable for LoginUriView { - fn encrypt_with_key(self, key: &SymmetricCryptoKey) -> Result { +impl KeyEncryptable for LoginUriView { + fn encrypt_with_key(self, key: &SymmetricCryptoKey) -> Result { Ok(LoginUri { uri: self.uri.encrypt_with_key(key)?, r#match: self.r#match, @@ -73,8 +73,8 @@ impl KeyEncryptable for LoginUriView { } } -impl KeyEncryptable for LoginView { - fn encrypt_with_key(self, key: &SymmetricCryptoKey) -> Result { +impl KeyEncryptable for LoginView { + fn encrypt_with_key(self, key: &SymmetricCryptoKey) -> Result { Ok(Login { username: self.username.encrypt_with_key(key)?, password: self.password.encrypt_with_key(key)?, @@ -86,8 +86,8 @@ impl KeyEncryptable for LoginView { } } -impl KeyDecryptable for LoginUri { - fn decrypt_with_key(&self, key: &SymmetricCryptoKey) -> Result { +impl KeyDecryptable for LoginUri { + fn decrypt_with_key(&self, key: &SymmetricCryptoKey) -> Result { Ok(LoginUriView { uri: self.uri.decrypt_with_key(key)?, r#match: self.r#match, @@ -95,14 +95,14 @@ impl KeyDecryptable for LoginUri { } } -impl KeyDecryptable for Login { - fn decrypt_with_key(&self, key: &SymmetricCryptoKey) -> Result { +impl KeyDecryptable for Login { + fn decrypt_with_key(&self, key: &SymmetricCryptoKey) -> Result { Ok(LoginView { - username: self.username.decrypt_with_key(key)?, - password: self.password.decrypt_with_key(key)?, + username: self.username.decrypt_with_key(key).ok().flatten(), + password: self.password.decrypt_with_key(key).ok().flatten(), password_revision_date: self.password_revision_date, - uris: self.uris.decrypt_with_key(key)?, - totp: self.totp.decrypt_with_key(key)?, + uris: self.uris.decrypt_with_key(key).ok().flatten(), + totp: self.totp.decrypt_with_key(key).ok().flatten(), autofill_on_page_load: self.autofill_on_page_load, }) } diff --git a/crates/bitwarden/src/vault/cipher/secure_note.rs b/crates/bitwarden/src/vault/cipher/secure_note.rs index 5fbb81811..8f7069ee1 100644 --- a/crates/bitwarden/src/vault/cipher/secure_note.rs +++ b/crates/bitwarden/src/vault/cipher/secure_note.rs @@ -1,12 +1,10 @@ use bitwarden_api_api::models::CipherSecureNoteModel; +use bitwarden_crypto::{CryptoError, KeyDecryptable, KeyEncryptable, SymmetricCryptoKey}; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; use serde_repr::{Deserialize_repr, Serialize_repr}; -use crate::{ - crypto::{KeyDecryptable, KeyEncryptable, SymmetricCryptoKey}, - error::{Error, Result}, -}; +use crate::error::{Error, Result}; #[derive(Clone, Copy, Serialize_repr, Deserialize_repr, Debug, JsonSchema)] #[repr(u8)] @@ -29,16 +27,16 @@ pub struct SecureNoteView { r#type: SecureNoteType, } -impl KeyEncryptable for SecureNoteView { - fn encrypt_with_key(self, _key: &SymmetricCryptoKey) -> Result { +impl KeyEncryptable for SecureNoteView { + fn encrypt_with_key(self, _key: &SymmetricCryptoKey) -> Result { Ok(SecureNote { r#type: self.r#type, }) } } -impl KeyDecryptable for SecureNote { - fn decrypt_with_key(&self, _key: &SymmetricCryptoKey) -> Result { +impl KeyDecryptable for SecureNote { + fn decrypt_with_key(&self, _key: &SymmetricCryptoKey) -> Result { Ok(SecureNoteView { r#type: self.r#type, }) diff --git a/crates/bitwarden/src/vault/collection.rs b/crates/bitwarden/src/vault/collection.rs index 3ff5d1350..d20e5d729 100644 --- a/crates/bitwarden/src/vault/collection.rs +++ b/crates/bitwarden/src/vault/collection.rs @@ -1,13 +1,12 @@ use bitwarden_api_api::models::CollectionDetailsResponseModel; +use bitwarden_crypto::{ + CryptoError, EncString, KeyContainer, KeyDecryptable, LocateKey, SymmetricCryptoKey, +}; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; use uuid::Uuid; -use crate::{ - client::encryption_settings::EncryptionSettings, - crypto::{EncString, KeyDecryptable, LocateKey, SymmetricCryptoKey}, - error::{Error, Result}, -}; +use crate::error::{Error, Result}; #[derive(Serialize, Deserialize, Debug, JsonSchema)] #[serde(rename_all = "camelCase", deny_unknown_fields)] @@ -40,19 +39,19 @@ pub struct CollectionView { impl LocateKey for Collection { fn locate_key<'a>( &self, - enc: &'a EncryptionSettings, + enc: &'a dyn KeyContainer, _: &Option, ) -> Option<&'a SymmetricCryptoKey> { enc.get_key(&Some(self.organization_id)) } } -impl KeyDecryptable for Collection { - fn decrypt_with_key(&self, key: &SymmetricCryptoKey) -> Result { +impl KeyDecryptable for Collection { + fn decrypt_with_key(&self, key: &SymmetricCryptoKey) -> Result { Ok(CollectionView { id: self.id, organization_id: self.organization_id, - name: self.name.decrypt_with_key(key)?, + name: self.name.decrypt_with_key(key).ok().unwrap_or_default(), external_id: self.external_id.clone(), hide_passwords: self.hide_passwords, diff --git a/crates/bitwarden/src/vault/folder.rs b/crates/bitwarden/src/vault/folder.rs index 04a4bebf7..17d1d40aa 100644 --- a/crates/bitwarden/src/vault/folder.rs +++ b/crates/bitwarden/src/vault/folder.rs @@ -1,13 +1,13 @@ use bitwarden_api_api::models::FolderResponseModel; +use bitwarden_crypto::{ + CryptoError, EncString, KeyDecryptable, KeyEncryptable, LocateKey, SymmetricCryptoKey, +}; use chrono::{DateTime, Utc}; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; use uuid::Uuid; -use crate::{ - crypto::{EncString, KeyDecryptable, KeyEncryptable, LocateKey, SymmetricCryptoKey}, - error::{Error, Result}, -}; +use crate::error::{Error, Result}; #[derive(Serialize, Deserialize, Debug, JsonSchema)] #[serde(rename_all = "camelCase")] @@ -28,8 +28,8 @@ pub struct FolderView { } impl LocateKey for FolderView {} -impl KeyEncryptable for FolderView { - fn encrypt_with_key(self, key: &SymmetricCryptoKey) -> Result { +impl KeyEncryptable for FolderView { + fn encrypt_with_key(self, key: &SymmetricCryptoKey) -> Result { Ok(Folder { id: self.id, name: self.name.encrypt_with_key(key)?, @@ -39,11 +39,11 @@ impl KeyEncryptable for FolderView { } impl LocateKey for Folder {} -impl KeyDecryptable for Folder { - fn decrypt_with_key(&self, key: &SymmetricCryptoKey) -> Result { +impl KeyDecryptable for Folder { + fn decrypt_with_key(&self, key: &SymmetricCryptoKey) -> Result { Ok(FolderView { id: self.id, - name: self.name.decrypt_with_key(key)?, + name: self.name.decrypt_with_key(key).ok().unwrap_or_default(), revision_date: self.revision_date, }) } diff --git a/crates/bitwarden/src/vault/mod.rs b/crates/bitwarden/src/vault/mod.rs index 6ac43a4d0..dce7b0e04 100644 --- a/crates/bitwarden/src/vault/mod.rs +++ b/crates/bitwarden/src/vault/mod.rs @@ -6,7 +6,12 @@ mod send; #[cfg(feature = "mobile")] mod totp; -pub use cipher::{Cipher, CipherListView, CipherView}; +pub use cipher::{ + attachment::{ + Attachment, AttachmentEncryptResult, AttachmentFile, AttachmentFileView, AttachmentView, + }, + Cipher, CipherListView, CipherView, +}; pub use collection::{Collection, CollectionView}; pub use folder::{Folder, FolderView}; pub use password_history::{PasswordHistory, PasswordHistoryView}; diff --git a/crates/bitwarden/src/vault/password_history.rs b/crates/bitwarden/src/vault/password_history.rs index 786c7a8f2..2ec20116b 100644 --- a/crates/bitwarden/src/vault/password_history.rs +++ b/crates/bitwarden/src/vault/password_history.rs @@ -1,12 +1,12 @@ use bitwarden_api_api::models::CipherPasswordHistoryModel; +use bitwarden_crypto::{ + CryptoError, EncString, KeyDecryptable, KeyEncryptable, LocateKey, SymmetricCryptoKey, +}; use chrono::{DateTime, Utc}; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; -use crate::{ - crypto::{EncString, KeyDecryptable, KeyEncryptable, LocateKey, SymmetricCryptoKey}, - error::{Error, Result}, -}; +use crate::error::{Error, Result}; #[derive(Serialize, Deserialize, Debug, JsonSchema)] #[serde(rename_all = "camelCase", deny_unknown_fields)] @@ -25,8 +25,8 @@ pub struct PasswordHistoryView { } impl LocateKey for PasswordHistoryView {} -impl KeyEncryptable for PasswordHistoryView { - fn encrypt_with_key(self, key: &SymmetricCryptoKey) -> Result { +impl KeyEncryptable for PasswordHistoryView { + fn encrypt_with_key(self, key: &SymmetricCryptoKey) -> Result { Ok(PasswordHistory { password: self.password.encrypt_with_key(key)?, last_used_date: self.last_used_date, @@ -35,10 +35,13 @@ impl KeyEncryptable for PasswordHistoryView { } impl LocateKey for PasswordHistory {} -impl KeyDecryptable for PasswordHistory { - fn decrypt_with_key(&self, key: &SymmetricCryptoKey) -> Result { +impl KeyDecryptable for PasswordHistory { + fn decrypt_with_key( + &self, + key: &SymmetricCryptoKey, + ) -> Result { Ok(PasswordHistoryView { - password: self.password.decrypt_with_key(key)?, + password: self.password.decrypt_with_key(key).ok().unwrap_or_default(), last_used_date: self.last_used_date, }) } diff --git a/crates/bitwarden/src/vault/send.rs b/crates/bitwarden/src/vault/send.rs index ce100db3e..aba67e00c 100644 --- a/crates/bitwarden/src/vault/send.rs +++ b/crates/bitwarden/src/vault/send.rs @@ -1,18 +1,21 @@ -use base64::{engine::general_purpose::URL_SAFE_NO_PAD, Engine}; +use base64::{ + engine::general_purpose::{STANDARD, URL_SAFE_NO_PAD}, + Engine, +}; use bitwarden_api_api::models::{SendFileModel, SendResponseModel, SendTextModel}; +use bitwarden_crypto::{ + derive_shareable_key, generate_random_bytes, CryptoError, EncString, KeyDecryptable, + KeyEncryptable, LocateKey, SymmetricCryptoKey, +}; use chrono::{DateTime, Utc}; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; use serde_repr::{Deserialize_repr, Serialize_repr}; use uuid::Uuid; -use crate::{ - crypto::{ - derive_shareable_key, generate_random_bytes, EncString, KeyDecryptable, KeyEncryptable, - LocateKey, SymmetricCryptoKey, - }, - error::{CryptoError, Error, Result}, -}; +use crate::error::{Error, Result}; + +const SEND_ITERATIONS: u32 = 100_000; #[derive(Serialize, Deserialize, Debug, JsonSchema)] #[serde(rename_all = "camelCase", deny_unknown_fields)] @@ -97,7 +100,13 @@ pub struct SendView { pub notes: Option, /// Base64 encoded key pub key: Option, - pub password: Option, + /// Replace or add a password to an existing send. The SDK will always return None when + /// decrypting a [Send] + /// TODO: We should revisit this, one variant is to have `[Create, Update]SendView` DTOs. + pub new_password: Option, + /// Denote if an existing send has a password. The SDK will ignore this value when creating or + /// updating sends. + pub has_password: bool, pub r#type: SendType, pub file: Option, @@ -134,19 +143,19 @@ impl Send { pub(crate) fn get_key( send_key: &EncString, enc_key: &SymmetricCryptoKey, - ) -> Result { + ) -> Result { let key: Vec = send_key.decrypt_with_key(enc_key)?; Self::derive_shareable_key(&key) } - fn derive_shareable_key(key: &[u8]) -> Result { + fn derive_shareable_key(key: &[u8]) -> Result { let key = key.try_into().map_err(|_| CryptoError::InvalidKeyLen)?; Ok(derive_shareable_key(key, "send", Some("send"))) } } -impl KeyDecryptable for SendText { - fn decrypt_with_key(&self, key: &SymmetricCryptoKey) -> Result { +impl KeyDecryptable for SendText { + fn decrypt_with_key(&self, key: &SymmetricCryptoKey) -> Result { Ok(SendTextView { text: self.text.decrypt_with_key(key)?, hidden: self.hidden, @@ -154,8 +163,8 @@ impl KeyDecryptable for SendText { } } -impl KeyEncryptable for SendTextView { - fn encrypt_with_key(self, key: &SymmetricCryptoKey) -> Result { +impl KeyEncryptable for SendTextView { + fn encrypt_with_key(self, key: &SymmetricCryptoKey) -> Result { Ok(SendText { text: self.text.encrypt_with_key(key)?, hidden: self.hidden, @@ -163,8 +172,8 @@ impl KeyEncryptable for SendTextView { } } -impl KeyDecryptable for SendFile { - fn decrypt_with_key(&self, key: &SymmetricCryptoKey) -> Result { +impl KeyDecryptable for SendFile { + fn decrypt_with_key(&self, key: &SymmetricCryptoKey) -> Result { Ok(SendFileView { id: self.id.clone(), file_name: self.file_name.decrypt_with_key(key)?, @@ -174,8 +183,8 @@ impl KeyDecryptable for SendFile { } } -impl KeyEncryptable for SendFileView { - fn encrypt_with_key(self, key: &SymmetricCryptoKey) -> Result { +impl KeyEncryptable for SendFileView { + fn encrypt_with_key(self, key: &SymmetricCryptoKey) -> Result { Ok(SendFile { id: self.id.clone(), file_name: self.file_name.encrypt_with_key(key)?, @@ -186,10 +195,11 @@ impl KeyEncryptable for SendFileView { } impl LocateKey for Send {} -impl KeyDecryptable for Send { - fn decrypt_with_key(&self, key: &SymmetricCryptoKey) -> Result { - // For sends, we first decrypt the send key with the user key, and stretch it to it's full size - // For the rest of the fields, we ignore the provided SymmetricCryptoKey and the stretched key +impl KeyDecryptable for Send { + fn decrypt_with_key(&self, key: &SymmetricCryptoKey) -> Result { + // For sends, we first decrypt the send key with the user key, and stretch it to it's full + // size For the rest of the fields, we ignore the provided SymmetricCryptoKey and + // the stretched key let k: Vec = self.key.decrypt_with_key(key)?; let key = Send::derive_shareable_key(&k)?; @@ -197,14 +207,15 @@ impl KeyDecryptable for Send { id: self.id, access_id: self.access_id.clone(), - name: self.name.decrypt_with_key(&key)?, - notes: self.notes.decrypt_with_key(&key)?, + name: self.name.decrypt_with_key(&key).ok().unwrap_or_default(), + notes: self.notes.decrypt_with_key(&key).ok().flatten(), key: Some(URL_SAFE_NO_PAD.encode(k)), - password: self.password.clone(), + new_password: None, + has_password: self.password.is_some(), r#type: self.r#type, - file: self.file.decrypt_with_key(&key)?, - text: self.text.decrypt_with_key(&key)?, + file: self.file.decrypt_with_key(&key).ok().flatten(), + text: self.text.decrypt_with_key(&key).ok().flatten(), max_access_count: self.max_access_count, access_count: self.access_count, @@ -218,10 +229,11 @@ impl KeyDecryptable for Send { } } -impl KeyDecryptable for Send { - fn decrypt_with_key(&self, key: &SymmetricCryptoKey) -> Result { - // For sends, we first decrypt the send key with the user key, and stretch it to it's full size - // For the rest of the fields, we ignore the provided SymmetricCryptoKey and the stretched key +impl KeyDecryptable for Send { + fn decrypt_with_key(&self, key: &SymmetricCryptoKey) -> Result { + // For sends, we first decrypt the send key with the user key, and stretch it to it's full + // size For the rest of the fields, we ignore the provided SymmetricCryptoKey and + // the stretched key let key = Send::get_key(&self.key, key)?; Ok(SendListView { @@ -241,10 +253,11 @@ impl KeyDecryptable for Send { } impl LocateKey for SendView {} -impl KeyEncryptable for SendView { - fn encrypt_with_key(self, key: &SymmetricCryptoKey) -> Result { - // For sends, we first decrypt the send key with the user key, and stretch it to it's full size - // For the rest of the fields, we ignore the provided SymmetricCryptoKey and the stretched key +impl KeyEncryptable for SendView { + fn encrypt_with_key(self, key: &SymmetricCryptoKey) -> Result { + // For sends, we first decrypt the send key with the user key, and stretch it to it's full + // size For the rest of the fields, we ignore the provided SymmetricCryptoKey and + // the stretched key let k = match (self.key, self.id) { // Existing send, decrypt key (Some(k), _) => URL_SAFE_NO_PAD @@ -256,7 +269,7 @@ impl KeyEncryptable for SendView { key.to_vec() } // Existing send without key - _ => return Err(CryptoError::InvalidKey.into()), + _ => return Err(CryptoError::InvalidKey), }; let send_key = Send::derive_shareable_key(&k)?; @@ -267,7 +280,10 @@ impl KeyEncryptable for SendView { name: self.name.encrypt_with_key(&send_key)?, notes: self.notes.encrypt_with_key(&send_key)?, key: k.encrypt_with_key(key)?, - password: self.password.clone(), + password: self.new_password.map(|password| { + let password = bitwarden_crypto::pbkdf2(password.as_bytes(), &k, SEND_ITERATIONS); + STANDARD.encode(password) + }), r#type: self.r#type, file: self.file.encrypt_with_key(&send_key)?, @@ -345,10 +361,11 @@ impl TryFrom for SendText { #[cfg(test)] mod tests { + use bitwarden_crypto::{KeyDecryptable, KeyEncryptable}; + use super::{Send, SendText, SendTextView, SendType}; use crate::{ - client::{encryption_settings::EncryptionSettings, kdf::Kdf, UserLoginMethod}, - crypto::{KeyDecryptable, KeyEncryptable}, + client::{encryption_settings::EncryptionSettings, Kdf, UserLoginMethod}, vault::SendView, }; @@ -370,39 +387,12 @@ mod tests { let k = enc.get_key(&None).unwrap(); - // Create a send object, the only value we really care about here is the key - let send = Send { - id: Some("d7fb1e7f-9053-43c0-a02c-b0690098685a".parse().unwrap()), - access_id: Some("fx7711OQwEOgLLBpAJhoWg".into()), - name: "2.u5vXPAepUZ+4lI2vGGKiGg==|hEouC4SvCCb/ifzZzLcfSw==|E2VZUVffehczfIuRSlX2vnPRfflBtXef5tzsWvRrtfM=" - .parse() - .unwrap(), - notes: None, - key: "2.+1KUfOX8A83Xkwk1bumo/w==|Nczvv+DTkeP466cP/wMDnGK6W9zEIg5iHLhcuQG6s+M=|SZGsfuIAIaGZ7/kzygaVUau3LeOvJUlolENBOU+LX7g=" - .parse() - .unwrap(), - password: None, - r#type: super::SendType::File, - file: Some(super::SendFile { - id: Some("7f129hzwu0umkmnmsghkt486w96p749c".into()), - file_name: "2.pnszM3slsCVlOIzuWrfCpA==|85zCg+X8GODvIAPf1Yt3K75YP+ub3wVAi1UvwOVXhPgUo9Gsu23FJgYSOkyKu3Vr|OvTrOugwRH7Mp2BWSuPlfxovoWt9oDRdi1Qo3xHUcdQ=" - .parse() - .unwrap(), - size: Some("1251825".into()), - size_name: Some("1.19 MB".into()), - }), - text: None, - max_access_count: None, - access_count: 0, - disabled: false, - hide_email: false, - revision_date: "2023-08-25T09:14:53Z".parse().unwrap(), - deletion_date: "2023-09-25T09:14:53Z".parse().unwrap(), - expiration_date: None, - }; + let send_key = "2.+1KUfOX8A83Xkwk1bumo/w==|Nczvv+DTkeP466cP/wMDnGK6W9zEIg5iHLhcuQG6s+M=|SZGsfuIAIaGZ7/kzygaVUau3LeOvJUlolENBOU+LX7g=" + .parse() + .unwrap(); // Get the send key - let send_key = Send::get_key(&send.key, k).unwrap(); + let send_key = Send::get_key(&send_key, k).unwrap(); let send_key_b64 = send_key.to_base64(); assert_eq!(send_key_b64, "IR9ImHGm6rRuIjiN7csj94bcZR5WYTJj5GtNfx33zm6tJCHUl+QZlpNPba8g2yn70KnOHsAODLcR0um6E3MAlg=="); } @@ -458,7 +448,8 @@ mod tests { name: "Test".to_string(), notes: None, key: Some("Pgui0FK85cNhBGWHAlBHBw".to_owned()), - password: None, + new_password: None, + has_password: false, r#type: SendType::Text, file: None, text: Some(SendTextView { @@ -488,7 +479,8 @@ mod tests { name: "Test".to_string(), notes: None, key: Some("Pgui0FK85cNhBGWHAlBHBw".to_owned()), - password: None, + new_password: None, + has_password: false, r#type: SendType::Text, file: None, text: Some(SendTextView { @@ -515,7 +507,7 @@ mod tests { } #[test] - pub fn test_encrypt_no_key() { + pub fn test_create() { let enc = build_encryption_settings(); let key = enc.get_key(&None).unwrap(); @@ -525,7 +517,8 @@ mod tests { name: "Test".to_string(), notes: None, key: None, - password: None, + new_password: None, + has_password: false, r#type: SendType::Text, file: None, text: Some(SendTextView { @@ -553,4 +546,44 @@ mod tests { let t = SendView { key: None, ..v }; assert_eq!(t, view); } + + #[test] + pub fn test_create_password() { + let enc = build_encryption_settings(); + let key = enc.get_key(&None).unwrap(); + + let view = SendView { + id: None, + access_id: Some("ct2APRQtJk-BLLDwAYqhRA".to_owned()), + name: "Test".to_owned(), + notes: None, + key: Some("Pgui0FK85cNhBGWHAlBHBw".to_owned()), + new_password: Some("abc123".to_owned()), + has_password: false, + r#type: SendType::Text, + file: None, + text: Some(SendTextView { + text: Some("This is a test".to_owned()), + hidden: false, + }), + max_access_count: None, + access_count: 0, + disabled: false, + hide_email: false, + revision_date: "2024-01-07T23:56:48.207363Z".parse().unwrap(), + deletion_date: "2024-01-14T23:56:48Z".parse().unwrap(), + expiration_date: None, + }; + + let send: Send = view.encrypt_with_key(key).unwrap(); + + assert_eq!( + send.password, + Some("vTIDfdj3FTDbejmMf+mJWpYdMXsxfeSd1Sma3sjCtiQ=".to_owned()) + ); + + let v: SendView = send.decrypt_with_key(key).unwrap(); + assert_eq!(v.new_password, None); + assert!(v.has_password); + } } diff --git a/crates/bitwarden/src/vault/totp.rs b/crates/bitwarden/src/vault/totp.rs index 9bc92229b..6ba754034 100644 --- a/crates/bitwarden/src/vault/totp.rs +++ b/crates/bitwarden/src/vault/totp.rs @@ -1,17 +1,18 @@ 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}; +use crate::error::{Error, Result}; + type HmacSha1 = Hmac; type HmacSha256 = Hmac; type HmacSha512 = Hmac; +const BASE32_CHARS: &str = "ABCDEFGHIJKLMNOPQRSTUVWXYZ234567"; const STEAM_CHARS: &str = "23456789BCDFGHJKMNPQRTVWXY"; const DEFAULT_ALGORITHM: Algorithm = Algorithm::Sha1; @@ -127,12 +128,6 @@ impl FromStr for Totp { /// - OTP Auth URI /// - Steam URI fn from_str(key: &str) -> Result { - fn decode_secret(secret: &str) -> Result> { - BASE32 - .decode(secret.to_uppercase().as_bytes()) - .map_err(|_| "Unable to decode secret".into()) - } - let params = if key.starts_with("otpauth://") { let url = Url::parse(key).map_err(|_| "Unable to parse URL")?; let parts: HashMap<_, _> = url.query_pairs().collect(); @@ -157,26 +152,26 @@ impl FromStr for Totp { .and_then(|v| v.parse().ok()) .map(|v: u32| v.max(1)) .unwrap_or(DEFAULT_PERIOD), - secret: decode_secret( + secret: decode_b32( &parts .get("secret") .map(|v| v.to_string()) .ok_or("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)?, + secret: decode_b32(secret), } } else { Totp { algorithm: DEFAULT_ALGORITHM, digits: DEFAULT_DIGITS, period: DEFAULT_PERIOD, - secret: decode_secret(key)?, + secret: decode_b32(key), } }; @@ -211,37 +206,73 @@ fn derive_binary(hash: Vec) -> u32 { | hash[offset + 3] as u32 } +/// This code is migrated from our javascript implementation and is not technically a correct base32 +/// decoder since we filter out various characters, and use exact chunking. +fn decode_b32(s: &str) -> Vec { + let s = s.to_uppercase(); + + let mut bits = String::new(); + for c in s.chars() { + if let Some(i) = BASE32_CHARS.find(c) { + bits.push_str(&format!("{:05b}", i)); + } + } + let mut bytes = Vec::new(); + + for chunk in bits.as_bytes().chunks_exact(8) { + let byte_str = std::str::from_utf8(chunk).unwrap(); + let byte = u8::from_str_radix(byte_str, 2).unwrap(); + bytes.push(byte); + } + + bytes +} + #[cfg(test)] mod tests { - use super::*; use chrono::Utc; + use super::*; + #[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(); + fn test_decode_b32() { + let res = decode_b32("WQIQ25BRKZYCJVYP"); + assert_eq!(res, vec![180, 17, 13, 116, 49, 86, 112, 36, 215, 15]); - assert_eq!(response.code, "194506".to_string()); - assert_eq!(response.period, 30); + let res = decode_b32("ABCD123"); + assert_eq!(res, vec![0, 68, 61]); } #[test] - fn test_lowercase_secret() { - let key = "wqiq25brkzycjvyp".to_string(); + fn test_generate_totp() { + let cases = vec![ + ("WQIQ25BRKZYCJVYP", "194506"), // valid base32 + ("wqiq25brkzycjvyp", "194506"), // lowercase + ("PIUDISEQYA", "829846"), // non padded + ("PIUDISEQYA======", "829846"), // padded + ("PIUD1IS!EQYA=", "829846"), // sanitized + // Steam + ("steam://HXDMVJECJJWSRB3HWIZR4IFUGFTMXBOZ", "7W6CJ"), + ("steam://ABCD123", "N26DF"), + // Various weird lengths + ("ddfdf", "932653"), + ("HJSGFJHDFDJDJKSDFD", "000034"), + ("xvdsfasdfasdasdghsgsdfg", "403786"), + ("KAKFJWOSFJ12NWL", "093430"), + ]; + 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); + for (key, expected_code) in cases { + let response = generate_totp(key.to_string(), time).unwrap(); + + assert_eq!(response.code, expected_code, "wrong code for key: {key}"); + assert_eq!(response.period, 30); + } } #[test] @@ -271,18 +302,4 @@ mod tests { 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/bitwarden/tests/register.rs b/crates/bitwarden/tests/register.rs index 956fe86ce..8e523e26f 100644 --- a/crates/bitwarden/tests/register.rs +++ b/crates/bitwarden/tests/register.rs @@ -5,10 +5,10 @@ async fn test_register_initialize_crypto() { use std::num::NonZeroU32; use bitwarden::{ - client::kdf::Kdf, mobile::crypto::{InitUserCryptoMethod, InitUserCryptoRequest}, Client, }; + use bitwarden_crypto::Kdf; let mut client = Client::new(None); diff --git a/crates/bitwarden/uniffi.toml b/crates/bitwarden/uniffi.toml index bc4a9d9ec..7a804ef1d 100644 --- a/crates/bitwarden/uniffi.toml +++ b/crates/bitwarden/uniffi.toml @@ -1,6 +1,7 @@ [bindings.kotlin] package_name = "com.bitwarden.core" generate_immutable_records = true +android = true [bindings.swift] ffi_module_name = "BitwardenCoreFFI" diff --git a/crates/bw/Cargo.toml b/crates/bw/Cargo.toml index d1298cf43..210a6b80a 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.13", features = ["derive", "env"] } +clap = { version = "4.4.18", 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 236aef22d..0e7cd975e 100644 --- a/crates/bw/src/main.rs +++ b/crates/bw/src/main.rs @@ -1,7 +1,7 @@ use bitwarden::{ auth::RegisterRequest, client::client_settings::ClientSettings, - tool::{PassphraseGeneratorRequest, PasswordGeneratorRequest}, + generators::{PassphraseGeneratorRequest, PasswordGeneratorRequest}, }; use bitwarden_cli::{install_color_eyre, text_prompt_when_none, Color}; use clap::{command, Args, CommandFactory, Parser, Subcommand}; diff --git a/crates/bws/Cargo.toml b/crates/bws/Cargo.toml index 980fe684e..fd723d3e6 100644 --- a/crates/bws/Cargo.toml +++ b/crates/bws/Cargo.toml @@ -16,12 +16,12 @@ keywords = ["bitwarden", "secrets-manager", "cli"] bat = { version = "0.24.0", features = [ "regex-onig", ], default-features = false } -chrono = { version = "0.4.31", features = [ +chrono = { version = "0.4.33", features = [ "clock", "std", ], default-features = false } -clap = { version = "4.4.13", features = ["derive", "env", "string"] } -clap_complete = "4.4.6" +clap = { version = "4.4.18", features = ["derive", "env", "string"] } +clap_complete = "4.4.9" color-eyre = "0.6" comfy-table = "^7.1.0" directories = "5.0.1" diff --git a/crates/bws/Dockerfile b/crates/bws/Dockerfile new file mode 100644 index 000000000..d75494648 --- /dev/null +++ b/crates/bws/Dockerfile @@ -0,0 +1,34 @@ +############################################### +# Build stage # +############################################### +FROM --platform=$BUILDPLATFORM rust:1.73 AS build + +# Docker buildx supplies the value for this arg +ARG TARGETPLATFORM + +RUN apt-get update && apt-get install -y --no-install-recommends \ + ca-certificates \ + && rm -rf /var/lib/apt/lists/* + +# Copy required project files +COPY . /app + +# Build project +WORKDIR /app/crates/bws +RUN cargo build --release + +############################################### +# App stage # +############################################### +FROM debian:bookworm-slim + +ARG TARGETPLATFORM +LABEL com.bitwarden.product="bitwarden" + +# Copy built project from the build stage +WORKDIR /usr/local/bin +COPY --from=build /app/target/release/bws . +COPY --from=build /etc/ssl/certs /etc/ssl/certs + +ENTRYPOINT ["bws"] + diff --git a/crates/bws/Dockerfile.dockerignore b/crates/bws/Dockerfile.dockerignore new file mode 100644 index 000000000..50f4b1230 --- /dev/null +++ b/crates/bws/Dockerfile.dockerignore @@ -0,0 +1,4 @@ +* +!crates/* +!Cargo.toml +!Cargo.lock diff --git a/crates/bws/src/main.rs b/crates/bws/src/main.rs index 117186663..e55df5082 100644 --- a/crates/bws/src/main.rs +++ b/crates/bws/src/main.rs @@ -34,7 +34,7 @@ struct Cli { #[command(subcommand)] command: Option, - #[arg(short = 'o', long, global = true, value_enum, default_value_t = Output::JSON, help="Output format", hide = true)] + #[arg(short = 'o', long, global = true, value_enum, default_value_t = Output::JSON, help="Output format")] output: Output, #[arg(short = 'c', long, global = true, value_enum, default_value_t = Color::Auto, help="Use colors in the output")] diff --git a/crates/bws/src/render.rs b/crates/bws/src/render.rs index 83c5cd532..f9563c81b 100644 --- a/crates/bws/src/render.rs +++ b/crates/bws/src/render.rs @@ -42,7 +42,8 @@ pub(crate) fn serialize_response, const N: usiz match output { Output::JSON => { let mut text = serde_json::to_string_pretty(&data).unwrap(); - // Yaml/table/tsv serializations add a newline at the end, so we do the same here for consistency + // Yaml/table/tsv serializations add a newline at the end, so we do the same here for + // consistency text.push('\n'); pretty_print("json", &text, color); } @@ -110,7 +111,8 @@ fn pretty_print(language: &str, data: &str, color: bool) { } } -// We're using const generics for the array lengths to make sure the header count and value count match +// We're using const generics for the array lengths to make sure the header count and value count +// match pub(crate) trait TableSerialize: Sized { fn get_headers() -> [&'static str; N]; fn get_values(&self) -> Vec<[String; N]>; diff --git a/crates/sdk-schemas/src/main.rs b/crates/sdk-schemas/src/main.rs index 0cfc045a0..f9f074485 100644 --- a/crates/sdk-schemas/src/main.rs +++ b/crates/sdk-schemas/src/main.rs @@ -3,8 +3,9 @@ use std::{fs::File, io::Write}; use anyhow::Result; 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. +/// 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. /// /// The schema filename is given by the last namespace element and trims off any `>` characters. /// This means the filename will represent the last _generic_ type of the type given. @@ -30,7 +31,8 @@ use schemars::{schema::RootSchema, schema_for, JsonSchema}; /// ``` /// write_schema_for!(response::two_factor_login_response::two_factor_providers::TwoFactorProviders); /// ``` -/// will generate `TwoFactorProviders.json` at `{{pwd}}/response/two_factor_login_response/TwoFactorProviders.json` +/// will generate `TwoFactorProviders.json` at +/// `{{pwd}}/response/two_factor_login_response/TwoFactorProviders.json` /// /// ## Path specified /// diff --git a/crates/uniffi-bindgen/Cargo.toml b/crates/uniffi-bindgen/Cargo.toml index cfbb5b554..e8f6f3f3d 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.2", features = ["cli"] } +uniffi = { version = "=0.26.1", features = ["cli"] } diff --git a/languages/csharp/Bitwarden.Sdk/Bitwarden.Sdk.csproj b/languages/csharp/Bitwarden.Sdk/Bitwarden.Sdk.csproj index 183ea8db9..3717aefdf 100644 --- a/languages/csharp/Bitwarden.Sdk/Bitwarden.Sdk.csproj +++ b/languages/csharp/Bitwarden.Sdk/Bitwarden.Sdk.csproj @@ -62,7 +62,7 @@ true runtimes/osx-arm64/native - + Always true runtimes/linux-x64/native diff --git a/languages/js/sdk-client/package-lock.json b/languages/js/sdk-client/package-lock.json index f1c72fa87..4f2b52824 100644 --- a/languages/js/sdk-client/package-lock.json +++ b/languages/js/sdk-client/package-lock.json @@ -39,9 +39,9 @@ } }, "node_modules/@types/node": { - "version": "18.19.5", - "resolved": "https://registry.npmjs.org/@types/node/-/node-18.19.5.tgz", - "integrity": "sha512-22MG6T02Hos2JWfa1o5jsIByn+bc5iOt1IS4xyg6OG68Bu+wMonVZzdrgCw693++rpLE9RUT/Bx15BeDzO0j+g==", + "version": "18.19.9", + "resolved": "https://registry.npmjs.org/@types/node/-/node-18.19.9.tgz", + "integrity": "sha512-oZFKlC8l5YtzGQNT4zC2PiSSKzQVZ8bAwwd+EYdPLtyk0nSEq6O16SkK+rkkT2eflDAbormJgEF3QnH3oDrTSw==", "dev": true, "dependencies": { "undici-types": "~5.26.4" diff --git a/languages/kotlin/app/src/main/java/com/bitwarden/myapplication/MainActivity.kt b/languages/kotlin/app/src/main/java/com/bitwarden/myapplication/MainActivity.kt index c38c14a07..f51d0309e 100644 --- a/languages/kotlin/app/src/main/java/com/bitwarden/myapplication/MainActivity.kt +++ b/languages/kotlin/app/src/main/java/com/bitwarden/myapplication/MainActivity.kt @@ -24,12 +24,12 @@ import androidx.compose.ui.unit.dp import androidx.fragment.app.FragmentActivity import com.bitwarden.core.DateTime import com.bitwarden.core.Folder -import com.bitwarden.core.HashPurpose import com.bitwarden.core.InitOrgCryptoRequest import com.bitwarden.core.InitUserCryptoMethod import com.bitwarden.core.InitUserCryptoRequest -import com.bitwarden.core.Kdf import com.bitwarden.core.Uuid +import com.bitwarden.crypto.HashPurpose +import com.bitwarden.crypto.Kdf import com.bitwarden.myapplication.ui.theme.MyApplicationTheme import com.bitwarden.sdk.Client import io.ktor.client.HttpClient diff --git a/languages/kotlin/doc.md b/languages/kotlin/doc.md index 27b376051..fd98e463e 100644 --- a/languages/kotlin/doc.md +++ b/languages/kotlin/doc.md @@ -138,6 +138,84 @@ password, use the email OTP. **Output**: std::result::Result<,BitwardenError> +### `new_auth_request` + +Initialize a new auth request + +**Arguments**: + +- self: +- email: String + +**Output**: std::result::Result + +### `approve_auth_request` + +Approve an auth request + +**Arguments**: + +- self: +- public_key: String + +**Output**: std::result::Result + +## ClientAttachments + +### `encrypt_buffer` + +Encrypt an attachment file in memory + +**Arguments**: + +- self: +- cipher: [Cipher](#cipher) +- attachment: [AttachmentView](#attachmentview) +- buffer: Vec<> + +**Output**: std::result::Result + +### `encrypt_file` + +Encrypt an attachment file located in the file system + +**Arguments**: + +- self: +- cipher: [Cipher](#cipher) +- attachment: [AttachmentView](#attachmentview) +- decrypted_file_path: String +- encrypted_file_path: String + +**Output**: std::result::Result + +### `decrypt_buffer` + +Decrypt an attachment file in memory + +**Arguments**: + +- self: +- cipher: [Cipher](#cipher) +- attachment: [Attachment](#attachment) +- buffer: Vec<> + +**Output**: std::result::Result + +### `decrypt_file` + +Decrypt an attachment file located in the file system + +**Arguments**: + +- self: +- cipher: [Cipher](#cipher) +- attachment: [Attachment](#attachment) +- encrypted_file_path: String +- decrypted_file_path: String + +**Output**: std::result::Result<,BitwardenError> + ## ClientCiphers ### `encrypt` @@ -246,6 +324,18 @@ initialize another client instance by using the PIN and the PIN key with **Output**: std::result::Result +### `derive_pin_user_key` + +Derives the pin protected user key from encrypted pin. Used when pin requires master password on +first unlock. + +**Arguments**: + +- self: +- encrypted_pin: [EncString](#encstring) + +**Output**: std::result::Result + ## ClientExporters ### `export_vault` @@ -529,6 +619,16 @@ Sends operations **Output**: Arc +### `attachments` + +Attachment file operations + +**Arguments**: + +- self: Arc + +**Output**: Arc + ### `generate_totp` Generate a TOTP code from a provided key. @@ -552,6 +652,86 @@ The key can be either: References are generated from the JSON schemas and should mostly match the kotlin and swift implementations. +## `Attachment` + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
KeyTypeDescription
idstring,null
urlstring,null
sizestring,null
sizeNamestring,nullReadable size, ex: "4.2 KB" or "1.43 GB"
fileName
key
+ +## `AttachmentView` + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
KeyTypeDescription
idstring,null
urlstring,null
sizestring,null
sizeNamestring,null
fileNamestring,null
key
+ ## `Cipher` @@ -852,6 +1032,16 @@ implementations.
+## `EncString` + + + + + + + +
KeyTypeDescription
+ ## `ExportFormat` @@ -1059,6 +1249,32 @@ implementations.
+ + authRequest + object + + + + + + + + + + + + + + + + + + + + +
KeyTypeDescription
request_private_keystringPrivate Key generated by the `crate::auth::new_auth_request`.
protected_user_keyUser Key protected by the private key provided in `AuthRequestResponse`.
+ + ## `InitUserCryptoRequest` @@ -1428,13 +1644,18 @@ implementations. key - - + string,null + Base64 encoded key - password + newPassword string,null - + Replace or add a password to an existing send. The SDK will always return None when decrypting a [Send] TODO: We should revisit this, one variant is to have `[Create, Update]SendView` DTOs. + + + hasPassword + boolean + Denote if an existing send has a password. The SDK will ignore this value when creating or updating sends. type diff --git a/languages/kotlin/sdk/build.gradle b/languages/kotlin/sdk/build.gradle index a4ee06814..9b3ca3585 100644 --- a/languages/kotlin/sdk/build.gradle +++ b/languages/kotlin/sdk/build.gradle @@ -30,6 +30,10 @@ android { jvmTarget = '1.8' } + lint { + baseline = file("lint-baseline.xml") + } + publishing { singleVariant('release') { withSourcesJar() diff --git a/languages/kotlin/sdk/lint-baseline.xml b/languages/kotlin/sdk/lint-baseline.xml new file mode 100644 index 000000000..3ae6c45f5 --- /dev/null +++ b/languages/kotlin/sdk/lint-baseline.xml @@ -0,0 +1,39 @@ + + + + + + + + + + + + + + + + + diff --git a/languages/php/src/BitwardenLib.php b/languages/php/src/BitwardenLib.php index 3eb3ed5f4..351728986 100644 --- a/languages/php/src/BitwardenLib.php +++ b/languages/php/src/BitwardenLib.php @@ -27,7 +27,7 @@ public function __construct() $lib_file = __DIR__.'/../../../target/debug/bitwarden_c.dll'; } } elseif (PHP_OS === 'Linux') { - $lib_file = '/lib/ubuntu-x64/libbitwarden_c.so'; + $lib_file = '/lib/linux-x64/libbitwarden_c.so'; if (file_exists($lib_file) == false) { $lib_file = __DIR__.'/../../../target/debug/libbitwarden_c.so'; } diff --git a/languages/python/README.md b/languages/python/README.md index 871be274f..e77e7a8eb 100644 --- a/languages/python/README.md +++ b/languages/python/README.md @@ -1,31 +1,32 @@ -# Requirements - -- Python3 -- setuptools - ```bash - pip install setuptools - ``` -- setuptools_rust - ```bash - pip install setuptools_rust - ``` -- dateutil - ```bash - pip install python-dateutil - ``` - -# Installation - -From the `languages/python/` directory, +# Build locally +## Requirements +- Python 3 +- `maturin` (install with `pip install maturin[patchelf]`) +- `npm` + +## Build + +From the root of the repository: ```bash -python3 ./setup.py develop +npm run schemas # generate schemas.py + +cd languages/python/ +maturin develop ``` -Rename the the resulting `.so` file to `bitwarden_py.so`, if it isn't already there. +You can now import `BitwardenClient` in your Python code. + +# Use without building locally + +```bash +pip install BitwardenClient +``` # Run +Set the `ORGANIZATION_ID` and `ACCESS_TOKEN` environment variables to your organization ID and access token, respectively. + ```bash -python3 ./login.py +python3 ./example.py ``` diff --git a/languages/python/bitwarden_sdk/__init__.py b/languages/python/bitwarden_sdk/__init__.py index e69de29bb..b2aeffea1 100644 --- a/languages/python/bitwarden_sdk/__init__.py +++ b/languages/python/bitwarden_sdk/__init__.py @@ -0,0 +1,13 @@ +"""The official Bitwarden client library for Python.""" + +__version__ = "0.1.0" + +from .bitwarden_client import * +from .schemas import * + +__doc__ = bitwarden_client.__doc__ +if hasattr(bitwarden_client, "__all__"): + __all__ = bitwarden_client.__all__ + +if hasattr(schemas, "__all__"): + __all__ += schemas.__all__ diff --git a/languages/python/bitwarden_sdk/bitwarden_client.py b/languages/python/bitwarden_sdk/bitwarden_client.py index cb669a40e..b5ea48c44 100644 --- a/languages/python/bitwarden_sdk/bitwarden_client.py +++ b/languages/python/bitwarden_sdk/bitwarden_client.py @@ -12,9 +12,10 @@ def __init__(self, settings: ClientSettings = None): settings_json = json.dumps(settings.to_dict()) self.inner = bitwarden_py.BitwardenClient(settings_json) - def access_token_login(self, access_token: str): + def access_token_login(self, access_token: str, + state_file_path: str = None): self._run_command( - Command(access_token_login=AccessTokenLoginRequest(access_token)) + Command(access_token_login=AccessTokenLoginRequest(access_token, state_file_path)) ) def secrets(self): diff --git a/languages/python/example.py b/languages/python/example.py old mode 100644 new mode 100755 index b3f2ab006..16367a0c5 --- a/languages/python/example.py +++ b/languages/python/example.py @@ -1,29 +1,35 @@ -import json +#!/usr/bin/env python3 import logging -import sys -from bitwarden_sdk.bitwarden_client import BitwardenClient -from bitwarden_sdk.schemas import client_settings_from_dict, DeviceType +import os + +from bitwarden_sdk import BitwardenClient, DeviceType, client_settings_from_dict # 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", -})) +client = BitwardenClient( + client_settings_from_dict( + { + "apiUrl": os.getenv("API_URL", "http://localhost:4000"), + "deviceType": DeviceType.SDK, + "identityUrl": os.getenv("IDENTITY_URL", "http://localhost:33656"), + "userAgent": "Python", + } + ) +) # Add some logging & set the org id logging.basicConfig(level=logging.DEBUG) -organization_id = "org_id_here" +organization_id = os.getenv("ORGANIZATION_ID") # Attempt to authenticate with the Secrets Manager Access Token -client.access_token_login("access_token_here") +client.access_token_login(os.getenv("ACCESS_TOKEN")) # -- 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) +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...") @@ -33,9 +39,28 @@ # -- 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 = 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...") diff --git a/languages/python/pyproject.toml b/languages/python/pyproject.toml new file mode 100644 index 000000000..28bb22507 --- /dev/null +++ b/languages/python/pyproject.toml @@ -0,0 +1,29 @@ +[build-system] +build-backend = "maturin" +requires = ["maturin>=1.0,<2.0", "setuptools_rust>=1.8.1"] + +[project] +authors = [{ name = "Bitwarden", email = "support@bitwarden.com" }] +classifiers = [ + "Development Status :: 4 - Beta", + "Intended Audience :: Developers", + "License :: Other/Proprietary License", + "Programming Language :: Python :: 3 :: Only", + "Programming Language :: Rust", + "Topic :: Security", +] +dependencies = ["dateutils >= 0.6.6"] +description = "A Bitwarden Client for python" +name = "bitwarden_sdk" +readme = "README.md" +requires-python = ">=3.0" +version = "0.1.0" + +[tool.maturin] +bindings = "pyo3" +compatibility = "2_28" +include = [ + { path = "bitwarden_sdk/*.py", format = ["sdist", "wheel"] } +] +manifest-path = "../../crates/bitwarden-py/Cargo.toml" +python-packages = ["bitwarden_sdk"] diff --git a/languages/python/setup.py b/languages/python/setup.py deleted file mode 100644 index b243a4fe8..000000000 --- a/languages/python/setup.py +++ /dev/null @@ -1,12 +0,0 @@ -from setuptools import setup -from setuptools_rust import Binding, RustExtension - -setup( - 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=['bitwarden_sdk'], - zip_safe=False, -) diff --git a/languages/ruby/bitwarden_sdk/bitwarden-sdk.gemspec b/languages/ruby/bitwarden_sdk/bitwarden-sdk.gemspec index 596531035..d11a57f38 100644 --- a/languages/ruby/bitwarden_sdk/bitwarden-sdk.gemspec +++ b/languages/ruby/bitwarden_sdk/bitwarden-sdk.gemspec @@ -25,7 +25,7 @@ Gem::Specification.new do |spec| end end - spec.files += Dir.glob('lib/ubuntu-x64/**/*') + spec.files += Dir.glob('lib/linux-x64/**/*') spec.files += Dir.glob('lib/macos-x64/**/*') spec.files += Dir.glob('lib/windows-x64/**/*') spec.files += Dir.glob('lib/macos-arm64/**/*') diff --git a/languages/ruby/bitwarden_sdk/lib/bitwarden_lib.rb b/languages/ruby/bitwarden_sdk/lib/bitwarden_lib.rb index e09541788..8217d87b0 100644 --- a/languages/ruby/bitwarden_sdk/lib/bitwarden_lib.rb +++ b/languages/ruby/bitwarden_sdk/lib/bitwarden_lib.rb @@ -19,7 +19,7 @@ def self.mac_with_intel? 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__) + local_file = File.expand_path('linux-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__) diff --git a/languages/swift/build.sh b/languages/swift/build.sh index 69da98415..69bc9a881 100755 --- a/languages/swift/build.sh +++ b/languages/swift/build.sh @@ -28,14 +28,12 @@ cargo run -p uniffi-bindgen generate \ --out-dir tmp/bindings # Move generated swift bindings -mv ./tmp/bindings/BitwardenSDK.swift ./Sources/BitwardenSdk/ -mv ./tmp/bindings/BitwardenCore.swift ./Sources/BitwardenSdk/ +mv ./tmp/bindings/*.swift ./Sources/BitwardenSdk/ # Massage the generated files to fit xcframework mkdir tmp/Headers -mv ./tmp/bindings/BitwardenFFI.h ./tmp/Headers/ -mv ./tmp/bindings/BitwardenCoreFFI.h ./tmp/Headers/ -cat ./tmp/bindings/BitwardenFFI.modulemap ./tmp/bindings/BitwardenCoreFFI.modulemap > ./tmp/Headers/module.modulemap +mv ./tmp/bindings/*.h ./tmp/Headers/ +cat ./tmp/bindings/*.modulemap > ./tmp/Headers/module.modulemap # Build xcframework xcodebuild -create-xcframework \ diff --git a/package-lock.json b/package-lock.json index b2bda48e9..f7c429825 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,9 +9,9 @@ "version": "0.0.0", "license": "SEE LICENSE IN LICENSE", "devDependencies": { - "@openapitools/openapi-generator-cli": "2.7.0", + "@openapitools/openapi-generator-cli": "2.9.0", "handlebars": "^4.7.8", - "prettier": "3.1.1", + "prettier": "3.2.4", "quicktype-core": "23.0.81", "rimraf": "5.0.5", "ts-node": "10.9.2", @@ -19,9 +19,9 @@ } }, "node_modules/@babel/runtime": { - "version": "7.23.6", - "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.23.6.tgz", - "integrity": "sha512-zHd0eUrf5GZoOWVCXp6koAKQTfZV07eit6bGPmJgnZdnSAvvZee6zniW2XMF7Cmc4ISOOnPy3QaSiIJGJkVEDQ==", + "version": "7.23.8", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.23.8.tgz", + "integrity": "sha512-Y7KbAP984rn1VGMbGqKmBLio9V7y5Je9GvU4rQPCPinCyNfUcToxIXl06d59URp/F3LwinvODxab5N/G6qggkw==", "dev": true, "dependencies": { "regenerator-runtime": "^0.14.0" @@ -179,44 +179,38 @@ } }, "node_modules/@nestjs/axios": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/@nestjs/axios/-/axios-0.1.0.tgz", - "integrity": "sha512-b2TT2X6BFbnNoeteiaxCIiHaFcSbVW+S5yygYqiIq5i6H77yIU3IVuLdpQkHq8/EqOWFwMopLN8jdkUT71Am9w==", + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@nestjs/axios/-/axios-3.0.1.tgz", + "integrity": "sha512-VlOZhAGDmOoFdsmewn8AyClAdGpKXQQaY1+3PGB+g6ceurGIdTxZgRX3VXc1T6Zs60PedWjg3A82TDOB05mrzQ==", "dev": true, - "dependencies": { - "axios": "0.27.2" - }, "peerDependencies": { - "@nestjs/common": "^7.0.0 || ^8.0.0 || ^9.0.0", + "@nestjs/common": "^7.0.0 || ^8.0.0 || ^9.0.0 || ^10.0.0", + "axios": "^1.3.1", "reflect-metadata": "^0.1.12", "rxjs": "^6.0.0 || ^7.0.0" } }, "node_modules/@nestjs/common": { - "version": "9.3.11", - "resolved": "https://registry.npmjs.org/@nestjs/common/-/common-9.3.11.tgz", - "integrity": "sha512-IFZ2G/5UKWC2Uo7tJ4SxGed2+aiA+sJyWeWsGTogKVDhq90oxVBToh+uCDeI31HNUpqYGoWmkletfty42zUd8A==", + "version": "10.3.0", + "resolved": "https://registry.npmjs.org/@nestjs/common/-/common-10.3.0.tgz", + "integrity": "sha512-DGv34UHsZBxCM3H5QGE2XE/+oLJzz5+714JQjBhjD9VccFlQs3LRxo/epso4l7nJIiNlZkPyIUC8WzfU/5RTsQ==", "dev": true, "dependencies": { "iterare": "1.2.1", - "tslib": "2.5.0", - "uid": "2.0.1" + "tslib": "2.6.2", + "uid": "2.0.2" }, "funding": { "type": "opencollective", "url": "https://opencollective.com/nest" }, "peerDependencies": { - "cache-manager": "<=5", "class-transformer": "*", "class-validator": "*", "reflect-metadata": "^0.1.12", "rxjs": "^7.1.0" }, "peerDependenciesMeta": { - "cache-manager": { - "optional": true - }, "class-transformer": { "optional": true }, @@ -225,16 +219,10 @@ } } }, - "node_modules/@nestjs/common/node_modules/tslib": { - "version": "2.5.0", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.5.0.tgz", - "integrity": "sha512-336iVw3rtn2BUK7ORdIAHTyxHGRIHVReokCR3XjbckJMK7ms8FysBfhLR8IXnAgy7T0PTPNBWKiH514FOW/WSg==", - "dev": true - }, "node_modules/@nestjs/core": { - "version": "9.3.11", - "resolved": "https://registry.npmjs.org/@nestjs/core/-/core-9.3.11.tgz", - "integrity": "sha512-CI27a2JFd5rvvbgkalWqsiwQNhcP4EAG5BUK8usjp29wVp1kx30ghfBT8FLqIgmkRVo65A0IcEnWsxeXMntkxQ==", + "version": "10.3.0", + "resolved": "https://registry.npmjs.org/@nestjs/core/-/core-10.3.0.tgz", + "integrity": "sha512-N06P5ncknW/Pm8bj964WvLIZn2gNhHliCBoAO1LeBvNImYkecqKcrmLbY49Fa1rmMfEM3MuBHeDys3edeuYAOA==", "dev": true, "hasInstallScript": true, "dependencies": { @@ -242,18 +230,18 @@ "fast-safe-stringify": "2.1.1", "iterare": "1.2.1", "path-to-regexp": "3.2.0", - "tslib": "2.5.0", - "uid": "2.0.1" + "tslib": "2.6.2", + "uid": "2.0.2" }, "funding": { "type": "opencollective", "url": "https://opencollective.com/nest" }, "peerDependencies": { - "@nestjs/common": "^9.0.0", - "@nestjs/microservices": "^9.0.0", - "@nestjs/platform-express": "^9.0.0", - "@nestjs/websockets": "^9.0.0", + "@nestjs/common": "^10.0.0", + "@nestjs/microservices": "^10.0.0", + "@nestjs/platform-express": "^10.0.0", + "@nestjs/websockets": "^10.0.0", "reflect-metadata": "^0.1.12", "rxjs": "^7.1.0" }, @@ -269,12 +257,6 @@ } } }, - "node_modules/@nestjs/core/node_modules/tslib": { - "version": "2.5.0", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.5.0.tgz", - "integrity": "sha512-336iVw3rtn2BUK7ORdIAHTyxHGRIHVReokCR3XjbckJMK7ms8FysBfhLR8IXnAgy7T0PTPNBWKiH514FOW/WSg==", - "dev": true - }, "node_modules/@nuxtjs/opencollective": { "version": "0.3.2", "resolved": "https://registry.npmjs.org/@nuxtjs/opencollective/-/opencollective-0.3.2.tgz", @@ -294,28 +276,29 @@ } }, "node_modules/@openapitools/openapi-generator-cli": { - "version": "2.7.0", - "resolved": "https://registry.npmjs.org/@openapitools/openapi-generator-cli/-/openapi-generator-cli-2.7.0.tgz", - "integrity": "sha512-ieEpHTA/KsDz7ANw03lLPYyjdedDEXYEyYoGBRWdduqXWSX65CJtttjqa8ZaB1mNmIjMtchUHwAYQmTLVQ8HYg==", + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/@openapitools/openapi-generator-cli/-/openapi-generator-cli-2.9.0.tgz", + "integrity": "sha512-KQpftKeiMoH5aEI/amOVLFGkGeT3DyA7Atj7v7l8xT3O9xnIUpoDmMg0WBTEh+NHxEwEAITQNDzr+JLjkXVaKw==", "dev": true, "hasInstallScript": true, "dependencies": { - "@nestjs/axios": "0.1.0", - "@nestjs/common": "9.3.11", - "@nestjs/core": "9.3.11", + "@nestjs/axios": "3.0.1", + "@nestjs/common": "10.3.0", + "@nestjs/core": "10.3.0", "@nuxtjs/opencollective": "0.3.2", + "axios": "1.6.5", "chalk": "4.1.2", "commander": "8.3.0", "compare-versions": "4.1.4", "concurrently": "6.5.1", "console.table": "0.10.0", "fs-extra": "10.1.0", - "glob": "7.1.6", - "inquirer": "8.2.5", + "glob": "7.2.3", + "inquirer": "8.2.6", "lodash": "4.17.21", "reflect-metadata": "0.1.13", - "rxjs": "7.8.0", - "tslib": "2.0.3" + "rxjs": "7.8.1", + "tslib": "2.6.2" }, "bin": { "openapi-generator-cli": "main.js" @@ -363,9 +346,9 @@ "dev": true }, "node_modules/@types/node": { - "version": "20.10.5", - "resolved": "https://registry.npmjs.org/@types/node/-/node-20.10.5.tgz", - "integrity": "sha512-nNPsNE65wjMxEKI93yOP+NPGGBJz/PoN3kZsVLee0XMiJolxSekEVD8wRwBUBqkwc7UWop0edW50yrCQW4CyRw==", + "version": "20.11.6", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.11.6.tgz", + "integrity": "sha512-+EOokTnksGVgip2PbYbr3xnR7kZigh4LbybAfBAw5BpnQ+FqBYUsvCEjYd70IXKlbohQ64mzEYmMtlWUY8q//Q==", "dev": true, "peer": true, "dependencies": { @@ -391,9 +374,9 @@ } }, "node_modules/acorn": { - "version": "8.11.2", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.11.2.tgz", - "integrity": "sha512-nc0Axzp/0FILLEVsm4fNwLCwMttvhEI263QtVPQcbpfZZ3ts0hLsZGOpE6czNlid7CJ9MlyH8reXkpsf3YUY4w==", + "version": "8.11.3", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.11.3.tgz", + "integrity": "sha512-Y9rRfJG5jcKOE0CLisYbojUjIrIEE7AGMzA/Sm4BslANhbS+cDMpgBdcPT91oJ7OuJ9hYJBx59RjbhxVnrF8Xg==", "dev": true, "bin": { "acorn": "bin/acorn" @@ -403,9 +386,9 @@ } }, "node_modules/acorn-walk": { - "version": "8.3.1", - "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.3.1.tgz", - "integrity": "sha512-TgUZgYvqZprrl7YldZNoa9OciCAyZR+Ejm9eXzKCmjsF5IKp/wgQ7Z/ZpjpGTIUPwrHQIcYeI8qDh4PsEwxMbw==", + "version": "8.3.2", + "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.3.2.tgz", + "integrity": "sha512-cjkyv4OtNCIeqhHrfS81QWXoCBPExR/J62oyEqepVw8WaQeSqpW2uhuLPh1m9eWhDuOo/jUXVTlifvesOWp/4A==", "dev": true, "engines": { "node": ">=0.4.0" @@ -463,13 +446,14 @@ "dev": true }, "node_modules/axios": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/axios/-/axios-0.27.2.tgz", - "integrity": "sha512-t+yRIyySRTp/wua5xEr+z1q60QmLq8ABsS5O9Me1AsE5dfKqgnCFzwiCZZ/cGNd1lq4/7akDWMxdhVlucjmnOQ==", + "version": "1.6.5", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.6.5.tgz", + "integrity": "sha512-Ii012v05KEVuUoFWmMW/UQv9aRIc3ZwkWDcM+h5Il8izZCtRVpDUfwpoFf7eOtajT3QiGR4yDUx7lPqHJULgbg==", "dev": true, "dependencies": { - "follow-redirects": "^1.14.9", - "form-data": "^4.0.0" + "follow-redirects": "^1.15.4", + "form-data": "^4.0.0", + "proxy-from-env": "^1.1.0" } }, "node_modules/balanced-match": { @@ -615,6 +599,23 @@ "wrap-ansi": "^7.0.0" } }, + "node_modules/cliui/node_modules/wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, "node_modules/clone": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/clone/-/clone-1.0.4.tgz", @@ -922,9 +923,9 @@ } }, "node_modules/follow-redirects": { - "version": "1.15.3", - "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.3.tgz", - "integrity": "sha512-1VzOtuEM8pC9SFU1E+8KfTjZyMztRsgEfwQl44z8A25uy13jSzTj6dyK2Df52iV0vgHCfBwLhDWevLn95w5v6Q==", + "version": "1.15.5", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.5.tgz", + "integrity": "sha512-vSFWUON1B+yAw1VN4xMfxgn5fTUiaOzAJCKBwIIgT/+7CuGy9+r+5gITvP62j3RmaD5Ph65UaERdOSRGUzZtgw==", "dev": true, "funding": [ { @@ -1013,15 +1014,15 @@ } }, "node_modules/glob": { - "version": "7.1.6", - "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.6.tgz", - "integrity": "sha512-LwaxwyZ72Lk7vZINtNNrywX0ZuLyStrdDtabefZKAY5ZGJhVtgdznluResxNmPitE0SAO+O26sWTHeKSI2wMBA==", + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", "dev": true, "dependencies": { "fs.realpath": "^1.0.0", "inflight": "^1.0.4", "inherits": "2", - "minimatch": "^3.0.4", + "minimatch": "^3.1.1", "once": "^1.3.0", "path-is-absolute": "^1.0.0" }, @@ -1117,9 +1118,9 @@ "dev": true }, "node_modules/inquirer": { - "version": "8.2.5", - "resolved": "https://registry.npmjs.org/inquirer/-/inquirer-8.2.5.tgz", - "integrity": "sha512-QAgPDQMEgrDssk1XiwwHoOGYF9BAbUcc1+j+FhEvaOt8/cKRqyLn0U5qA6F74fGhTMGxf92pOvPBeh29jQJDTQ==", + "version": "8.2.6", + "resolved": "https://registry.npmjs.org/inquirer/-/inquirer-8.2.6.tgz", + "integrity": "sha512-M1WuAmb7pn9zdFRtQYk26ZBoY043Sse0wVDdk4Bppr+JOXyQYybdtvK+l9wUibhtjdjvtoiNy8tk+EgsYIUqKg==", "dev": true, "dependencies": { "ansi-escapes": "^4.2.1", @@ -1136,7 +1137,7 @@ "string-width": "^4.1.0", "strip-ansi": "^6.0.0", "through": "^2.3.6", - "wrap-ansi": "^7.0.0" + "wrap-ansi": "^6.0.1" }, "engines": { "node": ">=12.0.0" @@ -1212,9 +1213,9 @@ } }, "node_modules/js-base64": { - "version": "3.7.5", - "resolved": "https://registry.npmjs.org/js-base64/-/js-base64-3.7.5.tgz", - "integrity": "sha512-3MEt5DTINKqfScXKfJFrRbxkrnk2AxPWGBL/ycjz4dK8iqiSJ06UxD8jh8xuh6p10TX4t2+7FsBYVxxQbMg+qA==", + "version": "3.7.6", + "resolved": "https://registry.npmjs.org/js-base64/-/js-base64-3.7.6.tgz", + "integrity": "sha512-NPrWuHFxFUknr1KqJRDgUQPexQF0uIJWjeT+2KjEePhitQxQEx5EJBG1lVn5/hc8aLycTpXrDOgPQ6Zq+EDiTA==", "dev": true }, "node_modules/jsonfile": { @@ -1470,9 +1471,9 @@ } }, "node_modules/prettier": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.1.1.tgz", - "integrity": "sha512-22UbSzg8luF4UuZtzgiUOfcGM8s4tjBv6dJRT7j275NXsy2jb4aJa4NNveul5x4eqlF1wuhuR2RElK71RvmVaw==", + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.2.4.tgz", + "integrity": "sha512-FWu1oLHKCrtpO1ypU6J0SbK2d9Ckwysq6bHj/uaCP26DxrPpppCLQRGVuqAxSTvhF00AcvDRyYrLNW7ocBhFFQ==", "dev": true, "bin": { "prettier": "bin/prettier.cjs" @@ -1493,6 +1494,12 @@ "node": ">= 0.6.0" } }, + "node_modules/proxy-from-env": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", + "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==", + "dev": true + }, "node_modules/quicktype-core": { "version": "23.0.81", "resolved": "https://registry.npmjs.org/quicktype-core/-/quicktype-core-23.0.81.tgz", @@ -1678,20 +1685,14 @@ } }, "node_modules/rxjs": { - "version": "7.8.0", - "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.0.tgz", - "integrity": "sha512-F2+gxDshqmIub1KdvZkaEfGDwLNpPvk9Fs6LD/MyQxNgMds/WH9OdDDXOmxUZpME+iSK3rQCctkL0DYyytUqMg==", + "version": "7.8.1", + "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.1.tgz", + "integrity": "sha512-AA3TVj+0A2iuIoQkWEK/tqFjBq2j+6PO6Y0zJcvzLAFhEFIO3HL0vls9hWLncZbAAbK0mar7oZ4V079I/qPMxg==", "dev": true, "dependencies": { "tslib": "^2.1.0" } }, - "node_modules/rxjs/node_modules/tslib": { - "version": "2.6.2", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz", - "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==", - "dev": true - }, "node_modules/safe-buffer": { "version": "5.2.1", "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", @@ -1918,9 +1919,9 @@ } }, "node_modules/tslib": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.0.3.tgz", - "integrity": "sha512-uZtkfKblCEQtZKBF6EBXVZeQNl82yqtDQdv+eck8u7tdPxjLu2/lp5/uPW+um2tpuxINHWy3GhiccY7QgEaVHQ==", + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz", + "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==", "dev": true }, "node_modules/type-fest": { @@ -1962,9 +1963,9 @@ } }, "node_modules/uid": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/uid/-/uid-2.0.1.tgz", - "integrity": "sha512-PF+1AnZgycpAIEmNtjxGBVmKbZAQguaa4pBUq6KNaGEcpzZ2klCNZLM34tsjp76maN00TttiiUf6zkIBpJQm2A==", + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/uid/-/uid-2.0.2.tgz", + "integrity": "sha512-u3xV3X7uzvi5b1MncmZo3i2Aw222Zk1keqLA1YkHldREkAhAqi65wuPfe7lHx8H/Wzy+8CE7S7uS3jekIM5s8g==", "dev": true, "dependencies": { "@lukeed/csprng": "^1.0.0" @@ -2080,9 +2081,9 @@ "dev": true }, "node_modules/wrap-ansi": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", - "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-6.2.0.tgz", + "integrity": "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==", "dev": true, "dependencies": { "ansi-styles": "^4.0.0", @@ -2090,10 +2091,7 @@ "strip-ansi": "^6.0.0" }, "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + "node": ">=8" } }, "node_modules/wrap-ansi-cjs": { diff --git a/package.json b/package.json index 319086900..14b5692a0 100644 --- a/package.json +++ b/package.json @@ -20,9 +20,9 @@ "test": "echo \"Error: no test specified\" && exit 1" }, "devDependencies": { - "@openapitools/openapi-generator-cli": "2.7.0", + "@openapitools/openapi-generator-cli": "2.9.0", "handlebars": "^4.7.8", - "prettier": "3.1.1", + "prettier": "3.2.4", "quicktype-core": "23.0.81", "rimraf": "5.0.5", "ts-node": "10.9.2", diff --git a/rustfmt.toml b/rustfmt.toml new file mode 100644 index 000000000..bb3baeccd --- /dev/null +++ b/rustfmt.toml @@ -0,0 +1,7 @@ +# Wrap comments and increase the width of comments to 100 +comment_width = 100 +wrap_comments = true + +# Sort and group imports +group_imports = "StdExternalCrate" +imports_granularity = "Crate" diff --git a/support/docs/docs.ts b/support/docs/docs.ts index b547b4502..067ff0827 100644 --- a/support/docs/docs.ts +++ b/support/docs/docs.ts @@ -21,6 +21,7 @@ const template = Handlebars.compile( const rootElements = [ "Client", "ClientAuth", + "ClientAttachments", "ClientCiphers", "ClientCollections", "ClientCrypto",