diff --git a/.github/workflows/build-android.yml b/.github/workflows/build-android.yml index cad899ef3..58a12f7f0 100644 --- a/.github/workflows/build-android.yml +++ b/.github/workflows/build-android.yml @@ -25,10 +25,10 @@ jobs: - target: i686-linux-android steps: - name: Checkout repo - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 + uses: actions/checkout@0ad4b8fadaa221de15dcec353f45205ec38ea70b # v4.1.4 - name: Install rust - uses: dtolnay/rust-toolchain@be73d7920c329f220ce78e0234b8f96b7ae60248 # stable + uses: dtolnay/rust-toolchain@d8352f6b1d2e870bc5716e7a6d9b65c4cc244a1a # stable with: toolchain: stable @@ -46,7 +46,7 @@ jobs: run: cross build -p bitwarden-uniffi --release --target=${{ matrix.settings.target }} - name: Upload artifact - uses: actions/upload-artifact@5d5d22a31266ced268874388b861e4b58bb5c2f3 # v4.3.1 + uses: actions/upload-artifact@65462800fd760344b1a7b4382951275a0abb4808 # v4.3.3 with: name: android-${{ matrix.settings.target }} path: ./target/${{ matrix.settings.target }}/release/libbitwarden_uniffi.so @@ -57,20 +57,20 @@ jobs: needs: build steps: - name: Checkout repo (PR) - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 + uses: actions/checkout@0ad4b8fadaa221de15dcec353f45205ec38ea70b # v4.1.4 if: github.event_name == 'pull_request' with: fetch-depth: 0 ref: ${{ github.event.pull_request.head.ref }} - name: Checkout repo (Push) - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 + uses: actions/checkout@0ad4b8fadaa221de15dcec353f45205ec38ea70b # v4.1.4 if: github.event_name == 'push' with: fetch-depth: 0 - name: Install rust - uses: dtolnay/rust-toolchain@be73d7920c329f220ce78e0234b8f96b7ae60248 # stable + uses: dtolnay/rust-toolchain@d8352f6b1d2e870bc5716e7a6d9b65c4cc244a1a # stable with: toolchain: stable @@ -80,13 +80,13 @@ jobs: key: cargo-combine-cache - name: Setup Java - uses: actions/setup-java@387ac29b308b003ca37ba93a6cab5eb57c8f5f93 # v4.0.0 + uses: actions/setup-java@99b8673ff64fbf99d8d325f52d9a5bdedb8483e9 # v4.2.1 with: distribution: temurin java-version: 17 - name: Download Artifacts - uses: actions/download-artifact@eaceaf801fd36c7dee90939fad912460b18a1ffe # v4.1.2 + uses: actions/download-artifact@65a9edc5881444af0b9093a5e628f2fe47ea3b2e # v4.1.7 - name: Move artifacts working-directory: languages/kotlin/sdk/src/main/jniLibs @@ -102,7 +102,7 @@ jobs: run: ./build-schemas.sh - name: Publish - uses: gradle/actions/setup-gradle@ec92e829475ac0c2315ea8f9eced72db85bb337a # v3.0.0 + uses: gradle/actions/setup-gradle@db19848a5fa7950289d3668fb053140cf3028d43 # v3.3.2 with: arguments: sdk:publish build-root-directory: languages/kotlin diff --git a/.github/workflows/build-cli-docker.yml b/.github/workflows/build-cli-docker.yml index 5cee3899b..375aa1bd9 100644 --- a/.github/workflows/build-cli-docker.yml +++ b/.github/workflows/build-cli-docker.yml @@ -3,13 +3,12 @@ name: Build bws Docker image on: push: - paths: - - "crates/bws/**" + branches: + - "main" + - "rc" + - "hotfix-rc" workflow_dispatch: pull_request: - paths: - - ".github/workflows/build-cli-docker.yml" - - "crates/bws/**" env: _AZ_REGISTRY: bitwardenprod.azurecr.io @@ -20,7 +19,7 @@ jobs: runs-on: ubuntu-22.04 steps: - name: Checkout Repository - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 + uses: actions/checkout@0ad4b8fadaa221de15dcec353f45205ec38ea70b # v4.1.4 - name: Check Branch to Publish env: @@ -42,7 +41,7 @@ jobs: uses: docker/setup-qemu-action@68827325e0b33c7199eb31dd4e31fbe9023e06e3 # v3.0.0 - name: Set up Docker Buildx - uses: docker/setup-buildx-action@f95db51fddba0c2d1ec667646a06c2ce06100226 # v3.0.0 + uses: docker/setup-buildx-action@d70bba72b1f3fd22344832f00baa16ece964efeb # v3.3.0 ########## Login to Docker registries ########## - name: Login to Azure - Prod Subscription @@ -99,14 +98,14 @@ jobs: fi - name: Build and push Docker image - uses: docker/build-push-action@4a13e500e55cf31b7a5d59a38ab2040ab0f42f56 # v5.1.0 + uses: docker/build-push-action@2cdde995de11925a030ce8070c3d77a52ffcf1c0 # v5.3.0 with: context: . file: crates/bws/Dockerfile platforms: | linux/amd64, linux/arm64/v8 - push: ${{ env.is_publish_branch }} + push: true tags: ${{ steps.tag-list.outputs.tags }} secrets: | "GH_PAT=${{ steps.retrieve-secret-pat.outputs.github-pat-bitwarden-devops-bot-repo-scope }}" @@ -150,7 +149,7 @@ jobs: secrets: "devops-alerts-slack-webhook-url" - name: Notify Slack on failure - uses: act10ns/slack@ed1309ab9862e57e9e583e51c7889486b9a00b0f # v2.0.0 + uses: act10ns/slack@44541246747a30eb3102d87f7a4cc5471b0ffb7d # v2.1.0 if: failure() env: SLACK_WEBHOOK_URL: ${{ steps.retrieve-secrets.outputs.devops-alerts-slack-webhook-url }} diff --git a/.github/workflows/build-cli.yml b/.github/workflows/build-cli.yml index e60928807..e9b1308e2 100644 --- a/.github/workflows/build-cli.yml +++ b/.github/workflows/build-cli.yml @@ -23,7 +23,7 @@ jobs: sign: ${{ steps.sign.outputs.sign }} steps: - name: Checkout repo - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 + uses: actions/checkout@0ad4b8fadaa221de15dcec353f45205ec38ea70b # v4.1.4 - name: Get Package Version id: retrieve-version @@ -58,16 +58,16 @@ jobs: target: aarch64-pc-windows-msvc steps: - name: Checkout repo - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 + uses: actions/checkout@0ad4b8fadaa221de15dcec353f45205ec38ea70b # v4.1.4 - name: Install rust - uses: dtolnay/rust-toolchain@be73d7920c329f220ce78e0234b8f96b7ae60248 # stable + uses: dtolnay/rust-toolchain@d8352f6b1d2e870bc5716e7a6d9b65c4cc244a1a # stable with: toolchain: stable 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 }} @@ -78,7 +78,7 @@ jobs: - name: Login to Azure if: ${{ needs.setup.outputs.sign == 'true' }} - uses: Azure/login@92a5484dfaf04ca78a94597f4f19fea633851fa2 # v1.4.7 + uses: Azure/login@e15b166166a8746d1a47596803bd8c1b595455cf # v1.6.0 with: creds: ${{ secrets.AZURE_KV_CI_SERVICE_PRINCIPAL }} @@ -123,7 +123,7 @@ jobs: run: 7z a ./bws-${{ matrix.settings.target }}-%_PACKAGE_VERSION%.zip ./target/${{ matrix.settings.target }}/release/bws.exe - name: Upload artifact - uses: actions/upload-artifact@c7d193f32edcb7bfad88892161225aeda64e9392 # v4.0.0 + uses: actions/upload-artifact@65462800fd760344b1a7b4382951275a0abb4808 # v4.3.3 with: name: bws-${{ matrix.settings.target }}-${{ env._PACKAGE_VERSION }}.zip path: ./bws-${{ matrix.settings.target }}-${{ env._PACKAGE_VERSION }}.zip @@ -148,16 +148,16 @@ jobs: steps: - name: Checkout repo - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 + uses: actions/checkout@0ad4b8fadaa221de15dcec353f45205ec38ea70b # v4.1.4 - name: Install rust - uses: dtolnay/rust-toolchain@be73d7920c329f220ce78e0234b8f96b7ae60248 # stable + uses: dtolnay/rust-toolchain@d8352f6b1d2e870bc5716e7a6d9b65c4cc244a1a # stable with: toolchain: stable 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 }} @@ -167,7 +167,7 @@ jobs: run: cargo build ${{ matrix.features }} -p bws --release --target=${{ matrix.settings.target }} - name: Login to Azure - uses: Azure/login@92a5484dfaf04ca78a94597f4f19fea633851fa2 # v1.4.7 + uses: Azure/login@e15b166166a8746d1a47596803bd8c1b595455cf # v1.6.0 with: creds: ${{ secrets.AZURE_KV_CI_SERVICE_PRINCIPAL }} @@ -233,7 +233,7 @@ jobs: xcrun notarytool submit ./bws-${{ matrix.settings.target }}-${{ env._PACKAGE_VERSION }}.zip --keychain-profile "notarytool-profile" --wait - name: Upload artifact - uses: actions/upload-artifact@c7d193f32edcb7bfad88892161225aeda64e9392 # v4.0.0 + uses: actions/upload-artifact@65462800fd760344b1a7b4382951275a0abb4808 # v4.3.3 with: name: bws-${{ matrix.settings.target }}-${{ env._PACKAGE_VERSION }}.zip path: ./bws-${{ matrix.settings.target }}-${{ env._PACKAGE_VERSION }}.zip @@ -250,6 +250,12 @@ jobs: fail-fast: false matrix: settings: + - os: ubuntu-20.04 + target: x86_64-unknown-linux-musl + + - os: ubuntu-20.04 + target: aarch64-unknown-linux-musl + - os: ubuntu-20.04 target: x86_64-unknown-linux-gnu @@ -257,40 +263,36 @@ jobs: target: aarch64-unknown-linux-gnu steps: - name: Checkout repo - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 + uses: actions/checkout@0ad4b8fadaa221de15dcec353f45205ec38ea70b # v4.1.4 - name: Install rust - uses: dtolnay/rust-toolchain@be73d7920c329f220ce78e0234b8f96b7ae60248 # stable + uses: dtolnay/rust-toolchain@d8352f6b1d2e870bc5716e7a6d9b65c4cc244a1a # stable with: toolchain: stable targets: ${{ matrix.settings.target }} + - uses: goto-bus-stop/setup-zig@7ab2955eb728f5440978d5824358023be3a2802d # v2.2.0 + with: + version: 0.12.0 + - name: Cache cargo registry 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 --git https://github.com/cross-rs/cross.git --rev 185398b1b885820515a212de720a306b08e2c8c9 + - name: Install Zigbuild + run: cargo install cargo-zigbuild --locked --git https://github.com/rust-cross/cargo-zigbuild --rev 6f7e1336c9cd13cf1b3704f93c40fcf84caaed6b # 0.18.4 - name: Build - if: ${{ matrix.settings.target != 'aarch64-unknown-linux-gnu' }} env: TARGET: ${{ matrix.settings.target }} - run: cargo build ${{ matrix.features }} -p bws --release --target=${{ matrix.settings.target }} - - - name: Build (aarch64-unknown-linux-gnu) - if: ${{ matrix.settings.target == 'aarch64-unknown-linux-gnu' }} - env: - TARGET: ${{ matrix.settings.target }} - run: cross build ${{ matrix.features }} -p bws --release --target=${{ matrix.settings.target }} + run: cargo zigbuild ${{ matrix.features }} -p bws --release --target=${{ matrix.settings.target }} - name: Zip linux run: zip -j ./bws-${{ matrix.settings.target }}-${{ env._PACKAGE_VERSION }}.zip ./target/${{ matrix.settings.target }}/release/bws - name: Upload artifact - uses: actions/upload-artifact@5d5d22a31266ced268874388b861e4b58bb5c2f3 # v4.3.1 + uses: actions/upload-artifact@65462800fd760344b1a7b4382951275a0abb4808 # v4.3.3 with: name: bws-${{ matrix.settings.target }}-${{ env._PACKAGE_VERSION }}.zip path: ./bws-${{ matrix.settings.target }}-${{ env._PACKAGE_VERSION }}.zip @@ -306,15 +308,15 @@ jobs: _PACKAGE_VERSION: ${{ needs.setup.outputs.package_version }} steps: - name: Checkout repo - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 + uses: actions/checkout@0ad4b8fadaa221de15dcec353f45205ec38ea70b # v4.1.4 - name: Download x86_64-apple-darwin artifact - uses: actions/download-artifact@eaceaf801fd36c7dee90939fad912460b18a1ffe # v4.1.2 + uses: actions/download-artifact@65a9edc5881444af0b9093a5e628f2fe47ea3b2e # v4.1.7 with: name: bws-x86_64-apple-darwin-${{ env._PACKAGE_VERSION }}.zip - name: Download aarch64-apple-darwin artifact - uses: actions/download-artifact@eaceaf801fd36c7dee90939fad912460b18a1ffe # v4.1.2 + uses: actions/download-artifact@65a9edc5881444af0b9093a5e628f2fe47ea3b2e # v4.1.7 with: name: bws-aarch64-apple-darwin-${{ env._PACKAGE_VERSION }}.zip @@ -330,7 +332,7 @@ jobs: lipo -create -output ./bws-macos-universal/bws ./bws-x86_64-apple-darwin/bws ./bws-aarch64-apple-darwin/bws - name: Login to Azure - uses: Azure/login@92a5484dfaf04ca78a94597f4f19fea633851fa2 # v1.4.7 + uses: Azure/login@e15b166166a8746d1a47596803bd8c1b595455cf # v1.6.0 with: creds: ${{ secrets.AZURE_KV_CI_SERVICE_PRINCIPAL }} @@ -395,7 +397,7 @@ jobs: xcrun notarytool submit ./bws-macos-universal-${{ env._PACKAGE_VERSION }}.zip --keychain-profile "notarytool-profile" --wait - name: Upload artifact - uses: actions/upload-artifact@5d5d22a31266ced268874388b861e4b58bb5c2f3 # v4.3.1 + uses: actions/upload-artifact@65462800fd760344b1a7b4382951275a0abb4808 # v4.3.3 with: name: bws-macos-universal-${{ env._PACKAGE_VERSION }}.zip path: ./bws-macos-universal-${{ env._PACKAGE_VERSION }}.zip @@ -408,10 +410,10 @@ jobs: - setup steps: - name: Checkout repo - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 + uses: actions/checkout@0ad4b8fadaa221de15dcec353f45205ec38ea70b # v4.1.4 - name: Install rust - uses: dtolnay/rust-toolchain@be73d7920c329f220ce78e0234b8f96b7ae60248 # stable + uses: dtolnay/rust-toolchain@d8352f6b1d2e870bc5716e7a6d9b65c4cc244a1a # stable with: toolchain: stable @@ -430,8 +432,40 @@ jobs: sed -i.bak 's/\$NAME\$/Bitwarden Secrets Manager CLI/g' THIRDPARTY.html - name: Upload artifact - uses: actions/upload-artifact@5d5d22a31266ced268874388b861e4b58bb5c2f3 # v4.3.1 + uses: actions/upload-artifact@65462800fd760344b1a7b4382951275a0abb4808 # v4.3.3 with: name: THIRDPARTY.html path: ./crates/bws/THIRDPARTY.html if-no-files-found: error + + manpages: + name: Generate manpages + runs-on: ubuntu-22.04 + needs: + - setup + steps: + - name: Checkout repo + uses: actions/checkout@0ad4b8fadaa221de15dcec353f45205ec38ea70b # v4.1.4 + + - name: Install rust + uses: dtolnay/rust-toolchain@d8352f6b1d2e870bc5716e7a6d9b65c4cc244a1a # stable + with: + toolchain: stable + + - name: Cache cargo registry + uses: Swatinem/rust-cache@23bce251a8cd2ffc3c1075eaa2367cf899916d84 # v2.7.3 + with: + key: cargo-cli-manpage + + - name: Generate manpages + run: | + cargo check -p bws --message-format json > build.json + OUT_DIR=$(jq -r --slurp '.[] | select (.reason == "build-script-executed") | select(.package_id|contains("crates/bws")) .out_dir' build.json) + mv $OUT_DIR/manpages . + + - name: Upload artifact + uses: actions/upload-artifact@65462800fd760344b1a7b4382951275a0abb4808 # v4.3.3 + with: + name: manpages + path: ./manpages/* + if-no-files-found: error diff --git a/.github/workflows/build-dotnet.yml b/.github/workflows/build-dotnet.yml index b08b37160..69987ab32 100644 --- a/.github/workflows/build-dotnet.yml +++ b/.github/workflows/build-dotnet.yml @@ -23,7 +23,7 @@ jobs: version: ${{ steps.version.outputs.version }} steps: - name: Checkout repo - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 + uses: actions/checkout@0ad4b8fadaa221de15dcec353f45205ec38ea70b # v4.1.4 - name: Install xmllint run: sudo apt-get install -y libxml2-utils @@ -44,10 +44,10 @@ jobs: steps: - name: Checkout Repository - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 + uses: actions/checkout@0ad4b8fadaa221de15dcec353f45205ec38ea70b # v4.1.4 - name: Download C# schemas artifact - uses: actions/download-artifact@eaceaf801fd36c7dee90939fad912460b18a1ffe # v4.1.2 + uses: actions/download-artifact@65a9edc5881444af0b9093a5e628f2fe47ea3b2e # v4.1.7 with: name: schemas.cs path: languages/csharp/Bitwarden.Sdk @@ -58,25 +58,25 @@ jobs: global-json-file: languages/csharp/global.json - name: Download x86_64-apple-darwin files - uses: actions/download-artifact@eaceaf801fd36c7dee90939fad912460b18a1ffe # v4.1.2 + uses: actions/download-artifact@65a9edc5881444af0b9093a5e628f2fe47ea3b2e # v4.1.7 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@eaceaf801fd36c7dee90939fad912460b18a1ffe # v4.1.2 + uses: actions/download-artifact@65a9edc5881444af0b9093a5e628f2fe47ea3b2e # v4.1.7 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@eaceaf801fd36c7dee90939fad912460b18a1ffe # v4.1.2 + uses: actions/download-artifact@65a9edc5881444af0b9093a5e628f2fe47ea3b2e # v4.1.7 with: name: libbitwarden_c_files-x86_64-unknown-linux-gnu path: languages/csharp/Bitwarden.Sdk/linux-x64 - name: Download x86_64-pc-windows-msvc files - uses: actions/download-artifact@eaceaf801fd36c7dee90939fad912460b18a1ffe # v4.1.2 + uses: actions/download-artifact@65a9edc5881444af0b9093a5e628f2fe47ea3b2e # v4.1.7 with: name: libbitwarden_c_files-x86_64-pc-windows-msvc path: languages/csharp/Bitwarden.Sdk/windows-x64 @@ -92,7 +92,7 @@ jobs: working-directory: languages/csharp/Bitwarden.Sdk - name: Upload NuGet package - uses: actions/upload-artifact@5d5d22a31266ced268874388b861e4b58bb5c2f3 # v4.3.1 + uses: actions/upload-artifact@65462800fd760344b1a7b4382951275a0abb4808 # v4.3.3 with: name: Bitwarden.Sdk.${{ needs.version.outputs.version }}.nupkg path: | diff --git a/.github/workflows/build-go.yaml b/.github/workflows/build-go.yaml index 433013aac..58918aeec 100644 --- a/.github/workflows/build-go.yaml +++ b/.github/workflows/build-go.yaml @@ -19,15 +19,15 @@ jobs: runs-on: ubuntu-22.04 steps: - name: Checkout Repository - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 + uses: actions/checkout@0ad4b8fadaa221de15dcec353f45205ec38ea70b # v4.1.4 - name: Setup Go environment - uses: actions/setup-go@0c52d547c9bc32b1aa3301fd7a9cb496313a4491 # v5.0.0 + uses: actions/setup-go@cdcb36043654635271a94b9a6d1392de5bb323a7 # v5.0.1 with: go-version: ${{ env.GO_VERSION }} - name: Cache dependencies - uses: actions/cache@13aacd865c20de90d75de3b17ebe84f7a17d57d2 # v4.0.0 + uses: actions/cache@0c45773b623bea8c8e75f6c82b208c3cf94ea4f9 # v4.0.2 with: path: ~/go/pkg/mod key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }} diff --git a/.github/workflows/build-java.yml b/.github/workflows/build-java.yml index 3b3c4ba5a..aa798fb4d 100644 --- a/.github/workflows/build-java.yml +++ b/.github/workflows/build-java.yml @@ -22,46 +22,46 @@ jobs: steps: - name: Checkout Repository - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 + uses: actions/checkout@0ad4b8fadaa221de15dcec353f45205ec38ea70b # v4.1.4 - name: Download Java schemas artifact - uses: actions/download-artifact@eaceaf801fd36c7dee90939fad912460b18a1ffe # v4.1.2 + uses: actions/download-artifact@65a9edc5881444af0b9093a5e628f2fe47ea3b2e # v4.1.7 with: name: sdk-schemas-java path: languages/java/src/main/java/bit/sdk/schema/ - name: Setup Java - uses: actions/setup-java@387ac29b308b003ca37ba93a6cab5eb57c8f5f93 # v4.0.0 + uses: actions/setup-java@99b8673ff64fbf99d8d325f52d9a5bdedb8483e9 # v4.2.1 with: distribution: temurin java-version: 17 - name: Download x86_64-apple-darwin files - uses: actions/download-artifact@eaceaf801fd36c7dee90939fad912460b18a1ffe # v4.1.2 + uses: actions/download-artifact@65a9edc5881444af0b9093a5e628f2fe47ea3b2e # v4.1.7 with: name: libbitwarden_c_files-x86_64-apple-darwin path: languages/java/src/main/resources/darwin-x86-64 - name: Download aarch64-apple-darwin files - uses: actions/download-artifact@eaceaf801fd36c7dee90939fad912460b18a1ffe # v4.1.2 + uses: actions/download-artifact@65a9edc5881444af0b9093a5e628f2fe47ea3b2e # v4.1.7 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@eaceaf801fd36c7dee90939fad912460b18a1ffe # v4.1.2 + uses: actions/download-artifact@65a9edc5881444af0b9093a5e628f2fe47ea3b2e # v4.1.7 with: name: libbitwarden_c_files-x86_64-unknown-linux-gnu path: languages/java/src/main/resources/linux-x86-64 - name: Download x86_64-pc-windows-msvc files - uses: actions/download-artifact@eaceaf801fd36c7dee90939fad912460b18a1ffe # v4.1.2 + uses: actions/download-artifact@65a9edc5881444af0b9093a5e628f2fe47ea3b2e # v4.1.7 with: name: libbitwarden_c_files-x86_64-pc-windows-msvc path: languages/java/src/main/resources/win32-x86-64 - name: Publish Maven - uses: gradle/actions/setup-gradle@ec92e829475ac0c2315ea8f9eced72db85bb337a # v3.0.0 + uses: gradle/actions/setup-gradle@db19848a5fa7950289d3668fb053140cf3028d43 # v3.3.2 with: arguments: publish build-root-directory: languages/java diff --git a/.github/workflows/build-napi.yml b/.github/workflows/build-napi.yml index aa1cfdd16..ccdc85385 100644 --- a/.github/workflows/build-napi.yml +++ b/.github/workflows/build-napi.yml @@ -51,7 +51,7 @@ jobs: strip *.node steps: - name: Checkout repo - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 + uses: actions/checkout@0ad4b8fadaa221de15dcec353f45205ec38ea70b # v4.1.4 - name: Setup Node uses: actions/setup-node@60edb5dd545a775178f52524783378180af0d1f8 # v4.0.2 @@ -61,7 +61,7 @@ jobs: cache-dependency-path: crates/bitwarden-napi/package-lock.json - name: Install rust - uses: dtolnay/rust-toolchain@be73d7920c329f220ce78e0234b8f96b7ae60248 # stable + uses: dtolnay/rust-toolchain@d8352f6b1d2e870bc5716e7a6d9b65c4cc244a1a # stable with: toolchain: stable targets: ${{ matrix.settings.target }} @@ -72,7 +72,7 @@ jobs: key: ${{ matrix.settings.target }}-cargo-${{ matrix.settings.os }} - name: Retrieve schemas - uses: actions/download-artifact@eaceaf801fd36c7dee90939fad912460b18a1ffe # v4.1.2 + uses: actions/download-artifact@65a9edc5881444af0b9093a5e628f2fe47ea3b2e # v4.1.7 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@5d5d22a31266ced268874388b861e4b58bb5c2f3 # v4.3.1 + uses: actions/upload-artifact@65462800fd760344b1a7b4382951275a0abb4808 # v4.3.3 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 index 7ffc2f24f..afb362426 100644 --- a/.github/workflows/build-python-wheels.yml +++ b/.github/workflows/build-python-wheels.yml @@ -26,7 +26,7 @@ jobs: package_version: ${{ steps.retrieve-version.outputs.package_version }} steps: - name: Checkout repo - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 + uses: actions/checkout@0ad4b8fadaa221de15dcec353f45205ec38ea70b # v4.1.4 - name: Get Package Version id: retrieve-version @@ -63,7 +63,7 @@ jobs: steps: - name: Checkout repo - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 + uses: actions/checkout@0ad4b8fadaa221de15dcec353f45205ec38ea70b # v4.1.4 - name: Setup Node uses: actions/setup-node@60edb5dd545a775178f52524783378180af0d1f8 # v4.0.2 @@ -71,7 +71,7 @@ jobs: node-version: 18 - name: Install rust - uses: dtolnay/rust-toolchain@be73d7920c329f220ce78e0234b8f96b7ae60248 # stable + uses: dtolnay/rust-toolchain@d8352f6b1d2e870bc5716e7a6d9b65c4cc244a1a # stable with: toolchain: stable targets: ${{ matrix.settings.target }} @@ -82,14 +82,14 @@ jobs: key: ${{ matrix.settings.target }}-cargo-${{ matrix.settings.os }} - name: Retrieve schemas - uses: actions/download-artifact@eaceaf801fd36c7dee90939fad912460b18a1ffe # v4.1.2 + uses: actions/download-artifact@65a9edc5881444af0b9093a5e628f2fe47ea3b2e # v4.1.7 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 + uses: PyO3/maturin-action@52b28abb0c6729beb388babfc348bf6ff5aaff31 # v1.42.2 with: target: ${{ matrix.settings.target }} args: --release --find-interpreter --sdist @@ -99,7 +99,7 @@ jobs: - name: Build wheels (Linux - x86_64) if: ${{ matrix.settings.target == 'x86_64-unknown-linux-gnu' }} - uses: PyO3/maturin-action@a3013db91b2ef2e51420cfe99ee619c8e72a17e6 # v1.40.8 + uses: PyO3/maturin-action@52b28abb0c6729beb388babfc348bf6ff5aaff31 # v1.42.2 with: target: ${{ matrix.settings.target }} args: --release --find-interpreter --sdist @@ -109,14 +109,14 @@ jobs: working-directory: ${{ github.workspace }}/languages/python - name: Upload wheels - uses: actions/upload-artifact@5d5d22a31266ced268874388b861e4b58bb5c2f3 # v4.3.1 + uses: actions/upload-artifact@65462800fd760344b1a7b4382951275a0abb4808 # v4.3.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@5d5d22a31266ced268874388b861e4b58bb5c2f3 # v4.3.1 + uses: actions/upload-artifact@65462800fd760344b1a7b4382951275a0abb4808 # v4.3.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 5d45b6f95..60a982814 100644 --- a/.github/workflows/build-rust-crates.yml +++ b/.github/workflows/build-rust-crates.yml @@ -36,10 +36,10 @@ jobs: steps: - name: Checkout - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 + uses: actions/checkout@0ad4b8fadaa221de15dcec353f45205ec38ea70b # v4.1.4 - name: Install rust - uses: dtolnay/rust-toolchain@be73d7920c329f220ce78e0234b8f96b7ae60248 # stable + uses: dtolnay/rust-toolchain@d8352f6b1d2e870bc5716e7a6d9b65c4cc244a1a # stable with: toolchain: stable targets: ${{ matrix.settings.target }} @@ -66,10 +66,10 @@ jobs: - build steps: - name: Checkout - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 + uses: actions/checkout@0ad4b8fadaa221de15dcec353f45205ec38ea70b # v4.1.4 - name: Install rust - uses: dtolnay/rust-toolchain@be73d7920c329f220ce78e0234b8f96b7ae60248 # stable + uses: dtolnay/rust-toolchain@d8352f6b1d2e870bc5716e7a6d9b65c4cc244a1a # stable with: toolchain: stable targets: ${{ matrix.settings.target }} diff --git a/.github/workflows/build-rust-cross-platform.yml b/.github/workflows/build-rust-cross-platform.yml index 0db2d0cde..46b481a65 100644 --- a/.github/workflows/build-rust-cross-platform.yml +++ b/.github/workflows/build-rust-cross-platform.yml @@ -29,10 +29,10 @@ jobs: steps: - name: Checkout - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 + uses: actions/checkout@0ad4b8fadaa221de15dcec353f45205ec38ea70b # v4.1.4 - name: Install rust - uses: dtolnay/rust-toolchain@be73d7920c329f220ce78e0234b8f96b7ae60248 # stable + uses: dtolnay/rust-toolchain@d8352f6b1d2e870bc5716e7a6d9b65c4cc244a1a # stable with: toolchain: stable @@ -48,7 +48,7 @@ jobs: run: cargo build --target ${{ matrix.settings.target }} --release - name: Upload Artifact - uses: actions/upload-artifact@5d5d22a31266ced268874388b861e4b58bb5c2f3 # v4.3.1 + uses: actions/upload-artifact@65462800fd760344b1a7b4382951275a0abb4808 # v4.3.3 with: name: libbitwarden_c_files-${{ matrix.settings.target }} path: | diff --git a/.github/workflows/build-wasm.yml b/.github/workflows/build-wasm.yml new file mode 100644 index 000000000..760e83254 --- /dev/null +++ b/.github/workflows/build-wasm.yml @@ -0,0 +1,76 @@ +--- +name: Build @bitwarden/sdk-wasm + +on: + pull_request: + push: + branches: + - "main" + - "rc" + - "hotfix-rc" + workflow_dispatch: + +defaults: + run: + shell: bash + working-directory: crates/bitwarden-wasm + +jobs: + build: + name: Building @bitwarden/sdk-wasm + runs-on: ubuntu-22.04 + + steps: + - name: Checkout repo + uses: actions/checkout@0ad4b8fadaa221de15dcec353f45205ec38ea70b # v4.1.4 + + - name: Setup Node + uses: actions/setup-node@60edb5dd545a775178f52524783378180af0d1f8 # v4.0.2 + with: + node-version: 18 + registry-url: "https://npm.pkg.github.com" + cache: "npm" + + - name: Install dependencies + run: npm i -g binaryen + + - name: Install rust + uses: dtolnay/rust-toolchain@d8352f6b1d2e870bc5716e7a6d9b65c4cc244a1a # stable + with: + toolchain: stable + targets: wasm32-unknown-unknown + + - name: Cache cargo registry + uses: Swatinem/rust-cache@23bce251a8cd2ffc3c1075eaa2367cf899916d84 # v2.7.3 + with: + key: wasm-cargo-cache + + - name: Install wasm-bindgen-cli + run: cargo install wasm-bindgen-cli + + - name: Build + run: ./build.sh -r + + - name: Upload artifact + uses: actions/upload-artifact@65462800fd760344b1a7b4382951275a0abb4808 # v4.3.3 + with: + name: sdk-bitwarden-wasm + path: ${{ github.workspace }}/languages/js/wasm/* + if-no-files-found: error + + - name: Set version + if: ${{ github.ref == 'refs/heads/main' }} + # Fetches current version from registry and uses prerelease to bump it + run: | + npm version --no-git-tag-version $(npm view @bitwarden/sdk-wasm@latest version) + npm version --no-git-tag-version prerelease + env: + NODE_AUTH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + working-directory: languages/js/wasm + + - name: Publish NPM + if: ${{ github.ref == 'refs/heads/main' }} + run: npm publish --access public + env: + NODE_AUTH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + working-directory: languages/js/wasm diff --git a/.github/workflows/cloc.yml b/.github/workflows/cloc.yml index f87ba6aa8..bba74c1dc 100644 --- a/.github/workflows/cloc.yml +++ b/.github/workflows/cloc.yml @@ -12,7 +12,7 @@ jobs: runs-on: ubuntu-22.04 steps: - name: Checkout repo - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 + uses: actions/checkout@0ad4b8fadaa221de15dcec353f45205ec38ea70b # v4.1.4 - name: Set up cloc run: | diff --git a/.github/workflows/direct-minimal-versions.yml b/.github/workflows/direct-minimal-versions.yml index 8db6e3c2e..34be69d36 100644 --- a/.github/workflows/direct-minimal-versions.yml +++ b/.github/workflows/direct-minimal-versions.yml @@ -36,10 +36,10 @@ jobs: steps: - name: Checkout - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 + uses: actions/checkout@0ad4b8fadaa221de15dcec353f45205ec38ea70b # v4.1.4 - name: Install rust - uses: dtolnay/rust-toolchain@be73d7920c329f220ce78e0234b8f96b7ae60248 # stable + uses: dtolnay/rust-toolchain@d8352f6b1d2e870bc5716e7a6d9b65c4cc244a1a # stable with: toolchain: nightly targets: ${{ matrix.settings.target }} diff --git a/.github/workflows/generate_schemas.yml b/.github/workflows/generate_schemas.yml index cc15bd4ff..008ee31e9 100644 --- a/.github/workflows/generate_schemas.yml +++ b/.github/workflows/generate_schemas.yml @@ -19,10 +19,10 @@ jobs: steps: - name: Checkout - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 + uses: actions/checkout@0ad4b8fadaa221de15dcec353f45205ec38ea70b # v4.1.4 - name: Install rust - uses: dtolnay/rust-toolchain@be73d7920c329f220ce78e0234b8f96b7ae60248 # stable + uses: dtolnay/rust-toolchain@d8352f6b1d2e870bc5716e7a6d9b65c4cc244a1a # stable with: toolchain: stable @@ -43,48 +43,48 @@ jobs: run: npm run schemas - name: Upload ts schemas artifact - uses: actions/upload-artifact@5d5d22a31266ced268874388b861e4b58bb5c2f3 # v4.3.1 + uses: actions/upload-artifact@65462800fd760344b1a7b4382951275a0abb4808 # v4.3.3 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@5d5d22a31266ced268874388b861e4b58bb5c2f3 # v4.3.1 + uses: actions/upload-artifact@65462800fd760344b1a7b4382951275a0abb4808 # v4.3.3 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@5d5d22a31266ced268874388b861e4b58bb5c2f3 # v4.3.1 + uses: actions/upload-artifact@65462800fd760344b1a7b4382951275a0abb4808 # v4.3.3 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@5d5d22a31266ced268874388b861e4b58bb5c2f3 # v4.3.1 + uses: actions/upload-artifact@65462800fd760344b1a7b4382951275a0abb4808 # v4.3.3 with: name: schemas.rb path: ${{ github.workspace }}/languages/ruby/bitwarden_sdk_secrets/lib/schemas.rb if-no-files-found: error - name: Upload json schemas artifact - uses: actions/upload-artifact@5d5d22a31266ced268874388b861e4b58bb5c2f3 # v4.3.1 + uses: actions/upload-artifact@65462800fd760344b1a7b4382951275a0abb4808 # v4.3.3 with: name: sdk-schemas-json path: ${{ github.workspace }}/support/schemas/* if-no-files-found: error - name: Upload Go schemas artifact - uses: actions/upload-artifact@5d5d22a31266ced268874388b861e4b58bb5c2f3 # v4.3.1 + uses: actions/upload-artifact@65462800fd760344b1a7b4382951275a0abb4808 # v4.3.3 with: name: schemas.go path: ${{ github.workspace }}/languages/go/schema.go - name: Upload java schemas artifact - uses: actions/upload-artifact@5d5d22a31266ced268874388b861e4b58bb5c2f3 # v4.3.1 + uses: actions/upload-artifact@65462800fd760344b1a7b4382951275a0abb4808 # v4.3.3 with: name: sdk-schemas-java path: ${{ github.workspace }}/languages/java/src/main/java/com/bitwarden/sdk/schema/* diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index 771b368f5..b9b6d4d73 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -17,10 +17,10 @@ jobs: steps: - name: Checkout - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 + uses: actions/checkout@0ad4b8fadaa221de15dcec353f45205ec38ea70b # v4.1.4 - name: Install rust - uses: dtolnay/rust-toolchain@be73d7920c329f220ce78e0234b8f96b7ae60248 # stable + uses: dtolnay/rust-toolchain@d8352f6b1d2e870bc5716e7a6d9b65c4cc244a1a # stable with: toolchain: stable @@ -45,7 +45,7 @@ jobs: RUSTFLAGS: "-D warnings" - name: Upload Clippy results to GitHub - uses: github/codeql-action/upload-sarif@47b3d888fe66b639e431abf22ebca059152f1eea # v3.24.5 + uses: github/codeql-action/upload-sarif@d39d31e687223d841ef683f52467bd88e9b21c14 # v3.25.3 with: sarif_file: clippy_result.sarif diff --git a/.github/workflows/memory-testing.yml b/.github/workflows/memory-testing.yml index af5ef6b7c..c0a9fbecd 100644 --- a/.github/workflows/memory-testing.yml +++ b/.github/workflows/memory-testing.yml @@ -22,7 +22,7 @@ jobs: steps: - name: Check out repo - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 + uses: actions/checkout@0ad4b8fadaa221de15dcec353f45205ec38ea70b # v4.1.4 - name: Set up gdb run: | @@ -30,7 +30,7 @@ jobs: sudo apt -y install gdb - name: Install rust - uses: dtolnay/rust-toolchain@be73d7920c329f220ce78e0234b8f96b7ae60248 # stable + uses: dtolnay/rust-toolchain@d8352f6b1d2e870bc5716e7a6d9b65c4cc244a1a # stable with: toolchain: stable diff --git a/.github/workflows/minimum-rust-version.yml b/.github/workflows/minimum-rust-version.yml index d3eccf653..af309ceed 100644 --- a/.github/workflows/minimum-rust-version.yml +++ b/.github/workflows/minimum-rust-version.yml @@ -27,10 +27,10 @@ jobs: steps: - name: Checkout - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 + uses: actions/checkout@0ad4b8fadaa221de15dcec353f45205ec38ea70b # v4.1.4 - name: Install rust - uses: dtolnay/rust-toolchain@be73d7920c329f220ce78e0234b8f96b7ae60248 # stable + uses: dtolnay/rust-toolchain@d8352f6b1d2e870bc5716e7a6d9b65c4cc244a1a # stable with: # Important: When updating this, make sure to update the Readme file # and also the `rust-version` field in all the `Cargo.toml`. diff --git a/.github/workflows/publish-dotnet.yml b/.github/workflows/publish-dotnet.yml index 73f930c1a..7a6573b05 100644 --- a/.github/workflows/publish-dotnet.yml +++ b/.github/workflows/publish-dotnet.yml @@ -24,7 +24,7 @@ jobs: version: ${{ steps.version.outputs.version }} steps: - name: Checkout repo - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 + uses: actions/checkout@0ad4b8fadaa221de15dcec353f45205ec38ea70b # v4.1.4 - name: Branch check if: ${{ inputs.release_type != 'Dry Run' }} @@ -60,7 +60,7 @@ jobs: path: ./nuget-output - name: Login to Azure - Prod Subscription - uses: Azure/login@92a5484dfaf04ca78a94597f4f19fea633851fa2 # v1.4.7 + uses: Azure/login@e15b166166a8746d1a47596803bd8c1b595455cf # v1.6.0 with: creds: ${{ secrets.AZURE_CI_SERVICE_PRINCIPAL }} diff --git a/.github/workflows/publish-php.yml b/.github/workflows/publish-php.yml index 782ffee1d..5323cffa0 100644 --- a/.github/workflows/publish-php.yml +++ b/.github/workflows/publish-php.yml @@ -24,7 +24,7 @@ jobs: version: ${{ steps.version.outputs.version }} steps: - name: Checkout repo - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 + uses: actions/checkout@0ad4b8fadaa221de15dcec353f45205ec38ea70b # v4.1.4 - name: Branch check if: ${{ inputs.release_type != 'Dry Run' }} @@ -47,10 +47,10 @@ jobs: runs-on: ubuntu-22.04 steps: - name: Checkout Repository - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 + uses: actions/checkout@0ad4b8fadaa221de15dcec353f45205ec38ea70b # v4.1.4 - name: Setup PHP with PECL extension - uses: shivammathur/setup-php@6d7209f44a25a59e904b1ee9f3b0c33ab2cd888d # 2.29.0 + uses: shivammathur/setup-php@c665c7a15b5295c2488ac8a87af9cb806cd72198 # 2.30.4 with: php-version: "8.0" tools: composer @@ -75,24 +75,24 @@ jobs: _PKG_VERSION: ${{ needs.validate.outputs.version }} steps: - name: Checkout SDK repo - uses: actions/checkout@1e31de5234b9f8995739874a8ce0492dc87873e2 # v4.0.0 + uses: actions/checkout@0ad4b8fadaa221de15dcec353f45205ec38ea70b # v4.1.4 with: path: sdk - name: Login to Azure - Prod Subscription - uses: Azure/login@92a5484dfaf04ca78a94597f4f19fea633851fa2 # v1.4.7 + uses: Azure/login@e15b166166a8746d1a47596803bd8c1b595455cf # v1.6.0 with: creds: ${{ secrets.AZURE_CI_SERVICE_PRINCIPAL }} - name: Retrieve secrets id: retrieve-secrets - uses: bitwarden/gh-actions/get-keyvault-secrets@4f37134d838f21609c38cb56694d8605f176704c + uses: bitwarden/gh-actions/get-keyvault-secrets@main with: keyvault: ${{ env._KEY_VAULT }} secrets: "github-pat-bitwarden-devops-bot-repo-scope" - name: Checkout SDK-PHP repo - uses: actions/checkout@1e31de5234b9f8995739874a8ce0492dc87873e2 # v4.0.0 + uses: actions/checkout@0ad4b8fadaa221de15dcec353f45205ec38ea70b # v4.1.4 with: repository: bitwarden/sm-sdk-php path: sm-sdk-php @@ -161,13 +161,13 @@ jobs: _PKG_VERSION: ${{ needs.validate.outputs.version }} steps: - name: Login to Azure - Prod Subscription - uses: Azure/login@92a5484dfaf04ca78a94597f4f19fea633851fa2 # v1.4.7 + uses: Azure/login@e15b166166a8746d1a47596803bd8c1b595455cf # v1.6.0 with: creds: ${{ secrets.AZURE_CI_SERVICE_PRINCIPAL }} - name: Retrieve secrets id: retrieve-secrets - uses: bitwarden/gh-actions/get-keyvault-secrets@4f37134d838f21609c38cb56694d8605f176704c + uses: bitwarden/gh-actions/get-keyvault-secrets@main with: keyvault: ${{ env._KEY_VAULT }} secrets: "github-pat-bitwarden-devops-bot-repo-scope" @@ -217,7 +217,7 @@ jobs: - name: Create release if: ${{ inputs.release_type != 'Dry Run' }} - uses: ncipollo/release-action@6c75be85e571768fa31b40abf38de58ba0397db5 # v1.13.0 + uses: ncipollo/release-action@2c591bcc8ecdcd2db72b97d6147f871fcd833ba5 # v1.14.0 with: tag: v${{ env._PKG_VERSION }} name: v${{ env._PKG_VERSION }} @@ -241,20 +241,20 @@ jobs: - github-release steps: - name: Login to Azure - Prod Subscription - uses: Azure/login@92a5484dfaf04ca78a94597f4f19fea633851fa2 # v1.4.7 + uses: Azure/login@e15b166166a8746d1a47596803bd8c1b595455cf # v1.6.0 with: creds: ${{ secrets.AZURE_CI_SERVICE_PRINCIPAL }} - name: Retrieve secrets id: retrieve-secrets - uses: bitwarden/gh-actions/get-keyvault-secrets@4f37134d838f21609c38cb56694d8605f176704c + uses: bitwarden/gh-actions/get-keyvault-secrets@main with: keyvault: ${{ env._KEY_VAULT }} secrets: "github-pat-bitwarden-devops-bot-repo-scope, packagist-key" - name: Checkout SDK-PHP repo - uses: actions/checkout@1e31de5234b9f8995739874a8ce0492dc87873e2 # v4.0.0 + uses: actions/checkout@0ad4b8fadaa221de15dcec353f45205ec38ea70b # v4.1.4 with: repository: bitwarden/sm-sdk-php path: sm-sdk-php diff --git a/.github/workflows/publish-python.yml b/.github/workflows/publish-python.yml index 780195161..6b421d0c2 100644 --- a/.github/workflows/publish-python.yml +++ b/.github/workflows/publish-python.yml @@ -24,7 +24,7 @@ jobs: runs-on: ubuntu-22.04 steps: - name: Checkout repo - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 + uses: actions/checkout@0ad4b8fadaa221de15dcec353f45205ec38ea70b # v4.1.4 - name: Branch check if: ${{ github.event.inputs.release_type != 'Dry Run' }} @@ -42,7 +42,7 @@ jobs: needs: setup steps: - name: Install Python - uses: actions/setup-python@0a5c61591373683505ea898e09a3ea4f39ef2b9c # v5.0.0 + uses: actions/setup-python@82c7e631bb3cdc910f68e0081d67478d79c6982d # v5.1.0 with: python-version: "3.9" @@ -50,7 +50,7 @@ jobs: run: pip install twine - name: Download artifacts - uses: dawidd6/action-download-artifact@e7466d1a7587ed14867642c2ca74b5bcc1e19a2d # v3.0.0 + uses: dawidd6/action-download-artifact@09f2f74827fd3a8607589e5ad7f9398816f540fe # v3.1.4 with: workflow: build-python-wheels.yml path: ${{ github.workspace }}/target/wheels/dist diff --git a/.github/workflows/publish-ruby.yml b/.github/workflows/publish-ruby.yml index b97a7b730..51d1cc765 100644 --- a/.github/workflows/publish-ruby.yml +++ b/.github/workflows/publish-ruby.yml @@ -23,7 +23,7 @@ jobs: runs-on: ubuntu-22.04 steps: - name: Checkout repo - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 + uses: actions/checkout@0ad4b8fadaa221de15dcec353f45205ec38ea70b # v4.1.4 - name: Branch check if: ${{ github.event.inputs.release_type != 'Dry Run' }} @@ -41,10 +41,10 @@ jobs: needs: setup steps: - name: Checkout Repository - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 + uses: actions/checkout@0ad4b8fadaa221de15dcec353f45205ec38ea70b # v4.1.4 - name: Set up Ruby - uses: ruby/setup-ruby@22fdc77bf4148f810455b226c90fb81b5cbc00a7 # v1.171.0 + uses: ruby/setup-ruby@cacc9f1c0b3f4eb8a16a6bb0ed10897b43b9de49 # v1.176.0 with: ruby-version: 3.2 diff --git a/.github/workflows/publish-rust-crates.yml b/.github/workflows/publish-rust-crates.yml index e3b2a1626..399810d9b 100644 --- a/.github/workflows/publish-rust-crates.yml +++ b/.github/workflows/publish-rust-crates.yml @@ -34,11 +34,21 @@ on: required: true default: true type: boolean + publish_bitwarden-cli: + description: "Publish bitwarden-cli crate" + required: true + default: true + type: boolean publish_bitwarden-generators: description: "Publish bitwarden-generators crate" required: true default: true type: boolean + publish_bitwarden-exporters: + description: "Publish bitwarden-exporters crate" + required: true + default: true + type: boolean defaults: run: @@ -53,7 +63,7 @@ jobs: packages_command: ${{ steps.packages-list.outputs.packages_command }} steps: - name: Checkout repo - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 + uses: actions/checkout@0ad4b8fadaa221de15dcec353f45205ec38ea70b # v4.1.4 - name: Branch check if: ${{ github.event.inputs.release_type != 'Dry Run' }} @@ -72,7 +82,9 @@ jobs: 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_CLI: ${{ github.event.inputs.publish_bitwarden-cli }} PUBLISH_BITWARDEN_GENERATORS: ${{ github.event.inputs.publish_bitwarden-generators }} + PUBLISH_BITWARDEN_EXPORTERS: ${{ github.event.inputs.publish_bitwarden-exporters }} run: | if [[ "$PUBLISH_BITWARDEN" == "false" ]] && [[ "$PUBLISH_BITWARDEN_API_API" == "false" ]] && [[ "$PUBLISH_BITWARDEN_API_IDENTITY" == "false" ]]; then echo "===================================" @@ -104,11 +116,21 @@ jobs: PACKAGES_LIST="$PACKAGES_LIST bitwarden-crypto" fi + if [[ "$PUBLISH_BITWARDEN_CLI" == "true" ]]; then + PACKAGES_COMMAND="$PACKAGES_COMMAND -p bitwarden-cli" + PACKAGES_LIST="$PACKAGES_LIST bitwarden-cli" + fi + if [[ "$PUBLISH_BITWARDEN_GENERATORS" == "true" ]]; then PACKAGES_COMMAND="$PACKAGES_COMMAND -p bitwarden-generators" PACKAGES_LIST="$PACKAGES_LIST bitwarden-generators" fi + if [[ "$PUBLISH_BITWARDEN_EXPORTERS" == "true" ]]; then + PACKAGES_COMMAND="$PACKAGES_COMMAND -p bitwarden-exporters" + PACKAGES_LIST="$PACKAGES_LIST bitwarden-exporters" + fi + echo "Packages command: " $PACKAGES_COMMAND echo "Packages list: " $PACKAGES_LIST @@ -122,7 +144,7 @@ jobs: - setup steps: - name: Checkout - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 + uses: actions/checkout@0ad4b8fadaa221de15dcec353f45205ec38ea70b # v4.1.4 - name: Login to Azure uses: Azure/login@e15b166166a8746d1a47596803bd8c1b595455cf # v1.6.0 @@ -137,7 +159,7 @@ jobs: secrets: "cratesio-api-token" - name: Install rust - uses: dtolnay/rust-toolchain@be73d7920c329f220ce78e0234b8f96b7ae60248 # stable + uses: dtolnay/rust-toolchain@d8352f6b1d2e870bc5716e7a6d9b65c4cc244a1a # stable with: toolchain: stable @@ -168,7 +190,7 @@ jobs: - name: Update deployment status to Success if: ${{ github.event.inputs.release_type != 'Dry Run' && success() }} - uses: chrnorm/deployment-status@2afb7d27101260f4a764219439564d954d10b5b0 # v2.0.1 + uses: chrnorm/deployment-status@9a72af4586197112e0491ea843682b5dc280d806 # v2.0.3 with: token: "${{ secrets.GITHUB_TOKEN }}" state: "success" @@ -176,7 +198,7 @@ jobs: - name: Update deployment status to Failure if: ${{ github.event.inputs.release_type != 'Dry Run' && failure() }} - uses: chrnorm/deployment-status@2afb7d27101260f4a764219439564d954d10b5b0 # v2.0.1 + uses: chrnorm/deployment-status@9a72af4586197112e0491ea843682b5dc280d806 # v2.0.3 with: token: "${{ secrets.GITHUB_TOKEN }}" state: "failure" diff --git a/.github/workflows/release-cli.yml b/.github/workflows/release-cli.yml index daf6dd622..df9715bb1 100644 --- a/.github/workflows/release-cli.yml +++ b/.github/workflows/release-cli.yml @@ -29,7 +29,7 @@ jobs: release-version: ${{ steps.version.outputs.version }} steps: - name: Checkout repo - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 + uses: actions/checkout@0ad4b8fadaa221de15dcec353f45205ec38ea70b # v4.1.4 - name: Branch check if: ${{ github.event.inputs.release_type != 'Dry Run' }} @@ -106,7 +106,7 @@ jobs: - name: Update deployment status to Success if: ${{ github.event.inputs.release_type != 'Dry Run' && success() }} - uses: chrnorm/deployment-status@2afb7d27101260f4a764219439564d954d10b5b0 # v2.0.1 + uses: chrnorm/deployment-status@9a72af4586197112e0491ea843682b5dc280d806 # v2.0.3 with: token: "${{ secrets.GITHUB_TOKEN }}" state: "success" @@ -114,7 +114,7 @@ jobs: - name: Update deployment status to Failure if: ${{ github.event.inputs.release_type != 'Dry Run' && failure() }} - uses: chrnorm/deployment-status@2afb7d27101260f4a764219439564d954d10b5b0 # v2.0.1 + uses: chrnorm/deployment-status@9a72af4586197112e0491ea843682b5dc280d806 # v2.0.3 with: token: "${{ secrets.GITHUB_TOKEN }}" state: "failure" @@ -127,7 +127,7 @@ jobs: - setup steps: - name: Checkout - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 + uses: actions/checkout@0ad4b8fadaa221de15dcec353f45205ec38ea70b # v4.1.4 - name: Login to Azure uses: Azure/login@e15b166166a8746d1a47596803bd8c1b595455cf # v1.6.0 @@ -142,7 +142,7 @@ jobs: secrets: "cratesio-api-token" - name: Install rust - uses: dtolnay/rust-toolchain@be73d7920c329f220ce78e0234b8f96b7ae60248 # stable + uses: dtolnay/rust-toolchain@d8352f6b1d2e870bc5716e7a6d9b65c4cc244a1a # stable with: toolchain: stable @@ -165,7 +165,7 @@ jobs: needs: setup steps: - name: Checkout - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 + uses: actions/checkout@0ad4b8fadaa221de15dcec353f45205ec38ea70b # v4.1.4 - name: Generate tag list id: tag-list @@ -186,7 +186,7 @@ jobs: uses: docker/setup-qemu-action@68827325e0b33c7199eb31dd4e31fbe9023e06e3 # v3.0.0 - name: Set up Docker Buildx - uses: docker/setup-buildx-action@f95db51fddba0c2d1ec667646a06c2ce06100226 # v3.0.0 + uses: docker/setup-buildx-action@d70bba72b1f3fd22344832f00baa16ece964efeb # v3.3.0 ########## Login to Docker registries ########## - name: Login to Azure - Prod Subscription @@ -216,7 +216,7 @@ jobs: azure-keyvault-name: "bitwarden-ci" - name: Build and push Docker image - uses: docker/build-push-action@4a13e500e55cf31b7a5d59a38ab2040ab0f42f56 # v5.1.0 + uses: docker/build-push-action@2cdde995de11925a030ce8070c3d77a52ffcf1c0 # v5.3.0 with: context: . file: crates/bws/Dockerfile diff --git a/.github/workflows/release-go.yml b/.github/workflows/release-go.yml index 8ea03e928..26a016d1d 100644 --- a/.github/workflows/release-go.yml +++ b/.github/workflows/release-go.yml @@ -26,7 +26,7 @@ jobs: version: ${{ steps.version.outputs.version }} steps: - name: Checkout repo - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 + uses: actions/checkout@0ad4b8fadaa221de15dcec353f45205ec38ea70b # v4.1.4 - name: Branch check if: ${{ inputs.release_type != 'Dry Run' }} @@ -54,12 +54,12 @@ jobs: _PKG_VERSION: ${{ needs.validate.outputs.version }} steps: - name: Checkout SDK repo - uses: actions/checkout@1e31de5234b9f8995739874a8ce0492dc87873e2 # v4.0.0 + uses: actions/checkout@0ad4b8fadaa221de15dcec353f45205ec38ea70b # v4.1.4 with: path: sdk - name: Login to Azure - Prod Subscription - uses: Azure/login@92a5484dfaf04ca78a94597f4f19fea633851fa2 # v1.4.7 + uses: Azure/login@e15b166166a8746d1a47596803bd8c1b595455cf # v1.6.0 with: creds: ${{ secrets.AZURE_CI_SERVICE_PRINCIPAL }} @@ -71,7 +71,7 @@ jobs: secrets: "github-pat-bitwarden-devops-bot-repo-scope" - name: Checkout SDK-Go repo - uses: actions/checkout@1e31de5234b9f8995739874a8ce0492dc87873e2 # v4.0.0 + uses: actions/checkout@0ad4b8fadaa221de15dcec353f45205ec38ea70b # v4.1.4 with: repository: bitwarden/sm-sdk-go path: sm-sdk-go @@ -145,7 +145,7 @@ jobs: _PKG_VERSION: ${{ needs.validate.outputs.version }} steps: - name: Login to Azure - Prod Subscription - uses: Azure/login@92a5484dfaf04ca78a94597f4f19fea633851fa2 # v1.4.7 + uses: Azure/login@e15b166166a8746d1a47596803bd8c1b595455cf # v1.6.0 with: creds: ${{ secrets.AZURE_CI_SERVICE_PRINCIPAL }} @@ -207,7 +207,7 @@ jobs: - name: Create release if: ${{ inputs.release_type != 'Dry Run' }} - uses: ncipollo/release-action@6c75be85e571768fa31b40abf38de58ba0397db5 # v1.13.0 + uses: ncipollo/release-action@2c591bcc8ecdcd2db72b97d6147f871fcd833ba5 # v1.14.0 with: tag: v${{ env._PKG_VERSION }} name: v${{ env._PKG_VERSION }} diff --git a/.github/workflows/release-napi.yml b/.github/workflows/release-napi.yml index de97249a3..f0f1bd667 100644 --- a/.github/workflows/release-napi.yml +++ b/.github/workflows/release-napi.yml @@ -33,7 +33,7 @@ jobs: release-version: ${{ steps.version.outputs.version }} steps: - name: Checkout repo - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 + uses: actions/checkout@0ad4b8fadaa221de15dcec353f45205ec38ea70b # v4.1.4 - name: Branch check if: ${{ github.event.inputs.release_type != 'Dry Run' }} @@ -67,7 +67,7 @@ jobs: - name: Update deployment status to Success if: ${{ github.event.inputs.release_type != 'Dry Run' && success() }} - uses: chrnorm/deployment-status@2afb7d27101260f4a764219439564d954d10b5b0 # v2.0.1 + uses: chrnorm/deployment-status@9a72af4586197112e0491ea843682b5dc280d806 # v2.0.3 with: token: "${{ secrets.GITHUB_TOKEN }}" state: "success" @@ -75,7 +75,7 @@ jobs: - name: Update deployment status to Failure if: ${{ github.event.inputs.release_type != 'Dry Run' && failure() }} - uses: chrnorm/deployment-status@2afb7d27101260f4a764219439564d954d10b5b0 # v2.0.1 + uses: chrnorm/deployment-status@9a72af4586197112e0491ea843682b5dc280d806 # v2.0.3 with: token: "${{ secrets.GITHUB_TOKEN }}" state: "failure" @@ -90,7 +90,7 @@ jobs: _PKG_VERSION: ${{ needs.setup.outputs.release-version }} steps: - name: Checkout repo - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 + uses: actions/checkout@0ad4b8fadaa221de15dcec353f45205ec38ea70b # v4.1.4 - name: Setup Node uses: actions/setup-node@60edb5dd545a775178f52524783378180af0d1f8 # v4.0.2 diff --git a/.github/workflows/release-wasm.yml b/.github/workflows/release-wasm.yml new file mode 100644 index 000000000..1511dd7be --- /dev/null +++ b/.github/workflows/release-wasm.yml @@ -0,0 +1,132 @@ +--- +name: Release @bitwarden/sdk-wasm +run-name: Release @bitwarden/sdk-wasm ${{ inputs.release_type }} + +on: + workflow_dispatch: + inputs: + release_type: + description: "Release Options" + required: true + default: "Release" + type: choice + options: + - Release + - Dry Run + npm_publish: + description: "Publish to NPM registry" + required: true + default: true + type: boolean + +defaults: + run: + shell: bash + working-directory: languages/js/wasm + +jobs: + setup: + name: Setup + runs-on: ubuntu-22.04 + outputs: + release-version: ${{ steps.version.outputs.version }} + steps: + - name: Checkout repo + uses: actions/checkout@0ad4b8fadaa221de15dcec353f45205ec38ea70b # v4.1.4 + + - name: Branch check + if: ${{ github.event.inputs.release_type != 'Dry Run' }} + run: | + if [[ "$GITHUB_REF" != "refs/heads/rc" ]] && [[ "$GITHUB_REF" != "refs/heads/hotfix-rc" ]]; then + echo "===================================" + echo "[!] Can only release from the 'rc' or 'hotfix-rc' branches" + echo "===================================" + exit 1 + fi + + - name: Check Release Version + id: version + uses: bitwarden/gh-actions/release-version-check@main + with: + release-type: ${{ github.event.inputs.release_type }} + project-type: ts + file: languages/js/wasm/package.json + monorepo: false + + - name: Create GitHub deployment + if: ${{ github.event.inputs.release_type != 'Dry Run' }} + uses: chrnorm/deployment-action@55729fcebec3d284f60f5bcabbd8376437d696b1 # v2.0.7 + id: deployment + with: + token: "${{ secrets.GITHUB_TOKEN }}" + initial-status: "in_progress" + environment: "Bitwarden SDK WASM - Production" + description: "Deployment ${{ steps.version.outputs.version }} from branch ${{ github.ref_name }}" + task: release + + - name: Update deployment status to Success + if: ${{ github.event.inputs.release_type != 'Dry Run' && success() }} + uses: chrnorm/deployment-status@9a72af4586197112e0491ea843682b5dc280d806 # v2.0.3 + with: + token: "${{ secrets.GITHUB_TOKEN }}" + state: "success" + deployment-id: ${{ steps.deployment.outputs.deployment_id }} + + - name: Update deployment status to Failure + if: ${{ github.event.inputs.release_type != 'Dry Run' && failure() }} + uses: chrnorm/deployment-status@9a72af4586197112e0491ea843682b5dc280d806 # v2.0.3 + with: + token: "${{ secrets.GITHUB_TOKEN }}" + state: "failure" + deployment-id: ${{ steps.deployment.outputs.deployment_id }} + + npm: + name: Publish NPM + runs-on: ubuntu-22.04 + needs: setup + if: inputs.npm_publish + env: + _PKG_VERSION: ${{ needs.setup.outputs.release-version }} + steps: + - name: Checkout repo + uses: actions/checkout@0ad4b8fadaa221de15dcec353f45205ec38ea70b # v4.1.4 + + - name: Setup Node + uses: actions/setup-node@60edb5dd545a775178f52524783378180af0d1f8 # v4.0.2 + with: + node-version: 18 + cache: "npm" + + - name: Login to Azure + uses: Azure/login@e15b166166a8746d1a47596803bd8c1b595455cf # v1.6.0 + with: + creds: ${{ secrets.AZURE_KV_CI_SERVICE_PRINCIPAL }} + + - name: Retrieve secrets + id: retrieve-secrets + uses: bitwarden/gh-actions/get-keyvault-secrets@main + with: + keyvault: "bitwarden-ci" + secrets: "npm-api-key" + + - name: Download artifacts + uses: bitwarden/gh-actions/download-artifacts@main + with: + workflow: build-wasm.yml + path: ${{ github.workspace }}/languages/js/wasm + workflow_conclusion: success + branch: ${{ github.event.inputs.release_type == 'Dry Run' && 'main' || github.ref_name }} + + - name: Setup NPM + run: | + echo 'registry="https://registry.npmjs.org/"' > ./.npmrc + echo "//registry.npmjs.org/:_authToken=$NPM_TOKEN" >> ./.npmrc + + echo 'registry="https://registry.npmjs.org/"' > ~/.npmrc + echo "//registry.npmjs.org/:_authToken=$NPM_TOKEN" >> ~/.npmrc + env: + NPM_TOKEN: ${{ steps.retrieve-secrets.outputs.npm-api-key }} + + - name: Publish NPM + if: ${{ github.event.inputs.release_type != 'Dry Run' }} + run: npm publish --access public --registry=https://registry.npmjs.org/ --userconfig=./.npmrc diff --git a/.github/workflows/rust-test.yml b/.github/workflows/rust-test.yml index 8408dd1d7..8aa844293 100644 --- a/.github/workflows/rust-test.yml +++ b/.github/workflows/rust-test.yml @@ -36,10 +36,10 @@ jobs: steps: - name: Checkout - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 + uses: actions/checkout@0ad4b8fadaa221de15dcec353f45205ec38ea70b # v4.1.4 - name: Install rust - uses: dtolnay/rust-toolchain@be73d7920c329f220ce78e0234b8f96b7ae60248 # stable + uses: dtolnay/rust-toolchain@d8352f6b1d2e870bc5716e7a6d9b65c4cc244a1a # stable with: toolchain: stable @@ -55,10 +55,10 @@ jobs: steps: - name: Checkout - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 + uses: actions/checkout@0ad4b8fadaa221de15dcec353f45205ec38ea70b # v4.1.4 - name: Install rust - uses: dtolnay/rust-toolchain@be73d7920c329f220ce78e0234b8f96b7ae60248 # stable + uses: dtolnay/rust-toolchain@d8352f6b1d2e870bc5716e7a6d9b65c4cc244a1a # stable with: toolchain: stable components: llvm-tools @@ -73,7 +73,7 @@ jobs: run: cargo llvm-cov --all-features --lcov --output-path lcov.info --ignore-filename-regex "crates/bitwarden-api-" - name: Upload to codecov.io - uses: codecov/codecov-action@e0b68c6749509c5f83f984dd99a76a1c1a231044 # v4.0.1 + uses: codecov/codecov-action@5ecb98a3c6b747ed38dc09f787459979aebb39be # v4.3.1 env: CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} @@ -84,10 +84,10 @@ jobs: steps: - name: Checkout - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 + uses: actions/checkout@0ad4b8fadaa221de15dcec353f45205ec38ea70b # v4.1.4 - name: Install rust - uses: dtolnay/rust-toolchain@be73d7920c329f220ce78e0234b8f96b7ae60248 # stable + uses: dtolnay/rust-toolchain@d8352f6b1d2e870bc5716e7a6d9b65c4cc244a1a # stable with: toolchain: stable targets: wasm32-unknown-unknown diff --git a/.github/workflows/scan.yml b/.github/workflows/scan.yml index 4bed1380f..11fd47651 100644 --- a/.github/workflows/scan.yml +++ b/.github/workflows/scan.yml @@ -26,12 +26,12 @@ jobs: steps: - name: Check out repo - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 + uses: actions/checkout@0ad4b8fadaa221de15dcec353f45205ec38ea70b # v4.1.4 with: ref: ${{ github.event.pull_request.head.sha }} - name: Scan with Checkmarx - uses: checkmarx/ast-github-action@749fec53e0db0f6404a97e2e0807c3e80e3583a7 #2.0.23 + uses: checkmarx/ast-github-action@dd0f9365942f29a99c3be5bdb308958ede8f906b # 2.0.25 env: INCREMENTAL: "${{ contains(github.event_name, 'pull_request') && '--sast-incremental' || '' }}" with: @@ -40,10 +40,13 @@ jobs: base_uri: https://ast.checkmarx.net/ cx_client_id: ${{ secrets.CHECKMARX_CLIENT_ID }} cx_client_secret: ${{ secrets.CHECKMARX_SECRET }} - additional_params: --report-format sarif --output-path . ${{ env.INCREMENTAL }} + additional_params: | + --report-format sarif \ + --filter "state=TO_VERIFY;PROPOSED_NOT_EXPLOITABLE;CONFIRMED;URGENT" \ + --output-path . ${{ env.INCREMENTAL }} - name: Upload Checkmarx results to GitHub - uses: github/codeql-action/upload-sarif@1b1aada464948af03b950897e5eb522f92603cc2 # v3.24.9 + uses: github/codeql-action/upload-sarif@d39d31e687223d841ef683f52467bd88e9b21c14 # v3.25.3 with: sarif_file: cx_result.sarif @@ -57,7 +60,7 @@ jobs: steps: - name: Check out repo - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 + uses: actions/checkout@0ad4b8fadaa221de15dcec353f45205ec38ea70b # v4.1.4 with: fetch-depth: 0 ref: ${{ github.event.pull_request.head.sha }} diff --git a/.github/workflows/version-bump.yml b/.github/workflows/version-bump.yml index 24692bea6..cc453c577 100644 --- a/.github/workflows/version-bump.yml +++ b/.github/workflows/version-bump.yml @@ -11,12 +11,7 @@ on: type: choice options: - bitwarden - - bitwarden-api-api - - bitwarden-api-identity - - bitwarden-crypto - - bitwarden-generators - - bitwarden-json - - cli + - bws - napi - python-sdk - ruby-sdk @@ -38,7 +33,7 @@ jobs: runs-on: ubuntu-22.04 steps: - name: Install rust - uses: dtolnay/rust-toolchain@be73d7920c329f220ce78e0234b8f96b7ae60248 # stable + uses: dtolnay/rust-toolchain@d8352f6b1d2e870bc5716e7a6d9b65c4cc244a1a # stable with: toolchain: stable @@ -46,7 +41,7 @@ jobs: uses: Swatinem/rust-cache@23bce251a8cd2ffc3c1075eaa2367cf899916d84 # v2.7.3 - name: Install cargo-release - run: cargo install cargo-edit + run: cargo install cargo-edit --locked - name: Login to Azure - CI Subscription uses: Azure/login@e15b166166a8746d1a47596803bd8c1b595455cf # v1.6.0 @@ -63,7 +58,7 @@ jobs: github-pat-bitwarden-devops-bot-repo-scope" - name: Checkout Branch - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 + uses: actions/checkout@0ad4b8fadaa221de15dcec353f45205ec38ea70b # v4.1.4 with: ref: main repository: bitwarden/sdk @@ -104,49 +99,19 @@ jobs: - name: Bump napi crate Version if: ${{ inputs.project == 'napi' }} - run: cargo-set-version set-version -p bitwarden-napi ${{ inputs.version_number }} + run: cargo set-version -p bitwarden-napi ${{ inputs.version_number }} ### bitwarden - name: Bump bitwarden crate Version if: ${{ inputs.project == 'bitwarden' }} - run: cargo-set-version set-version -p bitwarden ${{ inputs.version_number }} + run: cargo set-version -p bitwarden ${{ inputs.version_number }} - ### bitwarden-api-api + ### bws - - name: Bump bitwarden-api-api crate Version - if: ${{ inputs.project == 'bitwarden-api-api' }} - run: cargo-set-version set-version -p bitwarden-api-api ${{ inputs.version_number }} - - ### bitwarden-api-identity - - - name: Bump bitwarden-api-identity crate Version - 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 - if: ${{ inputs.project == 'cli' }} - run: cargo-set-version set-version -p bws ${{ inputs.version_number }} - - ### bitwarden-json - - - name: Bump bitwarden-json crate Version - if: ${{ inputs.project == 'bitwarden-json' }} - run: cargo-set-version set-version -p bitwarden-json ${{ inputs.version_number }} + - name: Bump bws Version + if: ${{ inputs.project == 'bws' }} + run: cargo set-version -p bws ${{ inputs.version_number }} ### python - name: Bump python-sdk Version @@ -261,7 +226,7 @@ jobs: runs-on: ubuntu-22.04 steps: - name: Checkout Branch - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 + uses: actions/checkout@0ad4b8fadaa221de15dcec353f45205ec38ea70b # v4.1.4 with: ref: main diff --git a/.prettierignore b/.prettierignore index 97474cca9..36c418776 100644 --- a/.prettierignore +++ b/.prettierignore @@ -1,8 +1,5 @@ target languages/* -!/languages/kotlin -languages/kotlin/* -!/languages/kotlin/doc.md schemas /crates/bitwarden-napi/src-ts/bitwarden_client/schemas.ts about.hbs diff --git a/.vscode/settings.json b/.vscode/settings.json index e92fcfb76..dcef4bc17 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -21,6 +21,8 @@ "totp", "uniffi", "wordlist", + "Zeroize", "zxcvbn" - ] + ], + "rust-analyzer.cargo.targetDir": true } diff --git a/Cargo.lock b/Cargo.lock index dfa714cc6..218c42894 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -112,15 +112,15 @@ dependencies = [ [[package]] name = "anyhow" -version = "1.0.81" +version = "1.0.82" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0952808a6c2afd1aa8947271f3a60f1a6763c7b912d210184c5149b5cf147247" +checksum = "f538837af36e6f6a9be0faa67f9a314f8119e4e4b5867c6ab40ed60360142519" [[package]] name = "arc-swap" -version = "1.7.0" +version = "1.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7b3d0060af21e8d11a926981cc00c6c1541aa91dd64b9f881985c3da1094425f" +checksum = "69f7f8c3906b62b754cd5326047894316021dcfe5a194c8ea52bdd94934a3457" [[package]] name = "argon2" @@ -158,7 +158,7 @@ dependencies = [ "proc-macro2", "quote", "serde", - "syn 2.0.53", + "syn 2.0.60", ] [[package]] @@ -212,26 +212,26 @@ dependencies = [ [[package]] name = "async-trait" -version = "0.1.78" +version = "0.1.80" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "461abc97219de0eaaf81fe3ef974a540158f3d079c2ab200f891f1a2ef201e85" +checksum = "c6fa2087f2753a7da8cc1c0dbfcf89579dd57458e36769de5ac750b4671737ca" dependencies = [ "proc-macro2", "quote", - "syn 2.0.53", + "syn 2.0.60", ] [[package]] name = "autocfg" -version = "1.1.0" +version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" +checksum = "f1fdabc7756949593fe60f30ec81974b613357de856987752631dea1e3394c80" [[package]] name = "backtrace" -version = "0.3.69" +version = "0.3.71" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2089b7e3f35b9dd2d0ed921ead4f6d318c27680d4a5bd167b3ee120edb105837" +checksum = "26b05800d2e817c8b3b4b54abd461726265fa9789ae34330622f2db9ee696f9d" dependencies = [ "addr2line", "cc", @@ -242,12 +242,24 @@ dependencies = [ "rustc-demangle", ] +[[package]] +name = "base16ct" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c7f02d4ea65f2c1853089ffd8d2787bdbc63de2f0d29dedbcf8ccdfa0ccd4cf" + [[package]] name = "base64" version = "0.21.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9d297deb1925b89f2ccc13d7635fa0714f12c87adce1c75356b39ca9b7178567" +[[package]] +name = "base64" +version = "0.22.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" + [[package]] name = "base64ct" version = "1.6.0" @@ -329,9 +341,10 @@ checksum = "cf4b9d6a944f767f8e5e0db018570623c85f3d925ac718db4e06d0187adb21c1" [[package]] name = "bitwarden" -version = "0.4.0" +version = "0.5.0" dependencies = [ - "base64", + "async-trait", + "base64 0.22.1", "bitwarden-api-api", "bitwarden-api-identity", "bitwarden-crypto", @@ -341,6 +354,7 @@ dependencies = [ "getrandom", "hmac", "log", + "passkey", "rand", "rand_chacha", "reqwest", @@ -363,7 +377,7 @@ dependencies = [ [[package]] name = "bitwarden-api-api" -version = "0.2.3" +version = "0.5.0" dependencies = [ "reqwest", "serde", @@ -376,7 +390,7 @@ dependencies = [ [[package]] name = "bitwarden-api-identity" -version = "0.2.3" +version = "0.5.0" dependencies = [ "reqwest", "serde", @@ -398,7 +412,7 @@ dependencies = [ [[package]] name = "bitwarden-cli" -version = "0.1.0" +version = "0.5.0" dependencies = [ "clap", "color-eyre", @@ -408,11 +422,11 @@ dependencies = [ [[package]] name = "bitwarden-crypto" -version = "0.1.0" +version = "0.5.0" dependencies = [ "aes", "argon2", - "base64", + "base64 0.22.1", "cbc", "generic-array", "hkdf", @@ -438,12 +452,13 @@ dependencies = [ [[package]] name = "bitwarden-exporters" -version = "0.1.0" +version = "0.5.0" dependencies = [ - "base64", + "base64 0.22.1", "bitwarden-crypto", "chrono", "csv", + "itertools 0.12.1", "serde", "serde_json", "thiserror", @@ -452,7 +467,7 @@ dependencies = [ [[package]] name = "bitwarden-generators" -version = "0.1.0" +version = "0.5.0" dependencies = [ "bitwarden-crypto", "rand", @@ -508,6 +523,7 @@ name = "bitwarden-uniffi" version = "0.1.0" dependencies = [ "async-lock", + "async-trait", "bitwarden", "bitwarden-crypto", "bitwarden-generators", @@ -515,12 +531,15 @@ dependencies = [ "env_logger", "schemars", "uniffi", + "uuid", ] [[package]] name = "bitwarden-wasm" version = "0.1.0" dependencies = [ + "argon2", + "bitwarden-crypto", "bitwarden-json", "console_error_panic_hook", "console_log", @@ -571,9 +590,9 @@ dependencies = [ [[package]] name = "bumpalo" -version = "3.15.4" +version = "3.16.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7ff69b9dd49fd426c69a0db9fc04dd934cdb6645ff000864d98f7e2af8830eaa" +checksum = "79296716171880943b8470b5f8d03aa55eb2e645a4874bdbb28adb49162e012c" [[package]] name = "bw" @@ -581,6 +600,7 @@ version = "0.0.2" dependencies = [ "bitwarden", "bitwarden-cli", + "bitwarden-crypto", "clap", "color-eyre", "env_logger", @@ -592,13 +612,15 @@ dependencies = [ [[package]] name = "bws" -version = "0.4.0" +version = "0.5.0" dependencies = [ "bat", "bitwarden", + "bitwarden-cli", "chrono", "clap", "clap_complete", + "clap_mangen", "color-eyre", "comfy-table", "directories", @@ -630,9 +652,9 @@ checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" [[package]] name = "bytes" -version = "1.5.0" +version = "1.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a2bd12c1caf447e69cd4528f47f94d203fd2582878ecb9e9465484c4148a8223" +checksum = "514de17de45fdb8dc022b1a7975556c53c86f9f0aa5f534b98977b171857c2c9" [[package]] name = "bytesize" @@ -651,9 +673,9 @@ dependencies = [ [[package]] name = "cargo-platform" -version = "0.1.7" +version = "0.1.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "694c8807f2ae16faecc43dc17d74b3eb042482789fd0eb64b39a2e04e087053f" +checksum = "24b1f0365a6c6bb4020cd05806fd0d33c44d38046b8bd7f0e40814b9763cabfc" dependencies = [ "serde", ] @@ -683,9 +705,9 @@ dependencies = [ [[package]] name = "cc" -version = "1.0.90" +version = "1.0.96" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8cd6604a82acf3039f1144f54b8eb34e91ffba622051189e71b781822d5ee1f5" +checksum = "065a29261d53ba54260972629f9ca6bffa69bac13cd1fed61420f7fa68b9f8bd" [[package]] name = "cesu8" @@ -701,9 +723,9 @@ checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" [[package]] name = "chrono" -version = "0.4.35" +version = "0.4.38" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8eaf5903dcbc0a39312feb77df2ff4c76387d591b9fc7b04a238dcf8bb62639a" +checksum = "a21f936df1771bf62b77f047b726c4625ff2e8aa607c01ec06e5a05bd8463401" dependencies = [ "android-tzdata", "iana-time-zone", @@ -711,7 +733,34 @@ dependencies = [ "num-traits", "serde", "wasm-bindgen", - "windows-targets 0.52.4", + "windows-targets 0.52.5", +] + +[[package]] +name = "ciborium" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42e69ffd6f0917f5c029256a24d0161db17cea3997d185db0d35926308770f0e" +dependencies = [ + "ciborium-io", + "ciborium-ll", + "serde", +] + +[[package]] +name = "ciborium-io" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05afea1e0a06c9be33d539b876f1ce3692f4afea2cb41f740e7743225ed1c757" + +[[package]] +name = "ciborium-ll" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57663b653d948a338bfb3eeba9bb2fd5fcfaecb9e199e87e1eda4d9e8b240fd9" +dependencies = [ + "ciborium-io", + "half", ] [[package]] @@ -727,9 +776,9 @@ dependencies = [ [[package]] name = "clap" -version = "4.5.3" +version = "4.5.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "949626d00e063efc93b6dca932419ceb5432f99769911c0b995f7e884c778813" +checksum = "90bc066a67923782aa8515dbaea16946c5bcc5addbd668bb80af688e53e548a0" dependencies = [ "clap_builder", "clap_derive", @@ -744,28 +793,28 @@ dependencies = [ "anstream", "anstyle", "clap_lex", - "strsim 0.11.0", + "strsim 0.11.1", ] [[package]] name = "clap_complete" -version = "4.5.1" +version = "4.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "885e4d7d5af40bfb99ae6f9433e292feac98d452dcb3ec3d25dfe7552b77da8c" +checksum = "dd79504325bf38b10165b02e89b4347300f855f273c4cb30c4a3209e6583275e" dependencies = [ "clap", ] [[package]] name = "clap_derive" -version = "4.5.3" +version = "4.5.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "90239a040c80f5e14809ca132ddc4176ab33d5e17e49691793296e3fcb34d72f" +checksum = "528131438037fd55894f62d6e9f068b8f45ac57ffa77517819645d10aed04f64" dependencies = [ "heck 0.5.0", "proc-macro2", "quote", - "syn 2.0.53", + "syn 2.0.60", ] [[package]] @@ -774,6 +823,16 @@ version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "98cc8fbded0c607b7ba9dd60cd98df59af97e84d24e49c8557331cfc26d301ce" +[[package]] +name = "clap_mangen" +version = "0.2.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e1dd95b5ebb5c1c54581dd6346f3ed6a79a3eef95dd372fc2ac13d535535300e" +dependencies = [ + "clap", + "roff", +] + [[package]] name = "clircle" version = "0.4.0" @@ -821,9 +880,9 @@ checksum = "acbf1af155f9b9ef647e42cdc158db4b64a1b61f743629225fde6f3e0be2a7c7" [[package]] name = "combine" -version = "4.6.6" +version = "4.6.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "35ed6e9d84f0b51a7f52daf1c7d71dd136fd7a3f41a8462b8cdb8c78d920fad4" +checksum = "ba5a308b75df32fe02788e748662718f03fde005016435c444eea572398219fd" dependencies = [ "bytes", "memchr", @@ -831,21 +890,21 @@ dependencies = [ [[package]] name = "comfy-table" -version = "7.1.0" +version = "7.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7c64043d6c7b7a4c58e39e7efccfdea7b93d885a795d0c054a69dbbf4dd52686" +checksum = "b34115915337defe99b2aff5c2ce6771e5fbc4079f4b506301f5cf394c8452f7" dependencies = [ "crossterm 0.27.0", - "strum", - "strum_macros", + "strum 0.26.2", + "strum_macros 0.26.2", "unicode-width", ] [[package]] name = "concurrent-queue" -version = "2.4.0" +version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d16048cd947b08fa32c24458a22f5dc5e835264f689f4f5653210c69fd107363" +checksum = "4ca0197aee26d1ae37445ee532fefce43251d24cc7c166799f4d46817f1d3973" dependencies = [ "crossbeam-utils", ] @@ -924,6 +983,16 @@ version = "0.8.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "06ea2b9bc92be3c2baa9334a323ebca2d6f074ff852cd1d7b11064035cd3868f" +[[package]] +name = "coset" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff8aad850c1f86daa47e812913051eb5a26c4d9fb4242a89178bf99b946e4e3c" +dependencies = [ + "ciborium", + "ciborium-io", +] + [[package]] name = "cpufeatures" version = "0.2.12" @@ -1005,6 +1074,24 @@ dependencies = [ "winapi", ] +[[package]] +name = "crunchy" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a81dae078cea95a014a339291cec439d2f232ebe854a9d672b796c6afafa9b7" + +[[package]] +name = "crypto-bigint" +version = "0.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0dc92fb57ca44df6db8059111ab3af99a63d5d0f8375d9972e319a379c6bab76" +dependencies = [ + "generic-array", + "rand_core", + "subtle", + "zeroize", +] + [[package]] name = "crypto-common" version = "0.1.6" @@ -1038,12 +1125,12 @@ dependencies = [ [[package]] name = "ctor" -version = "0.2.7" +version = "0.2.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ad291aa74992b9b7a7e88c38acbbf6ad7e107f1d90ee8775b7bc1fc3394f485c" +checksum = "edb49164822f3ee45b17acd4a208cfc1251410cf0cad9a833234c9890774dd9f" dependencies = [ "quote", - "syn 2.0.53", + "syn 2.0.60", ] [[package]] @@ -1081,6 +1168,12 @@ dependencies = [ "syn 1.0.109", ] +[[package]] +name = "data-encoding" +version = "2.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e8566979429cf69b49a5c740c60791108e86440e8be149bbea4fe54d2c32d6e2" + [[package]] name = "deadpool" version = "0.10.0" @@ -1101,9 +1194,9 @@ checksum = "63dfa964fe2a66f3fde91fc70b267fe193d822c7e603e2a675a49a7f46ad3f49" [[package]] name = "der" -version = "0.7.8" +version = "0.7.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fffa369a668c8af7dbf8b5e56c9f744fbd399949ed171606040001947de40b1c" +checksum = "f55bf8e7b65898637379c1b74eb1551107c8294ed26d855ceb9fd1a09cfc9bc0" dependencies = [ "const-oid", "pem-rfc7468", @@ -1189,11 +1282,48 @@ version = "1.0.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0d6ef0072f8a535281e4876be788938b528e9a1d43900b82c2569af7da799125" +[[package]] +name = "ecdsa" +version = "0.16.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee27f32b5c5292967d2d4a9d7f1e0b0aed2c15daded5a60300e4abb9d8020bca" +dependencies = [ + "der", + "digest", + "elliptic-curve", + "rfc6979", + "signature", + "spki", +] + [[package]] name = "either" -version = "1.10.0" +version = "1.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "11157ac094ffbdde99aa67b23417ebdd801842852b500e395a45a9c0aac03e4a" +checksum = "a47c1c47d2f5964e29c61246e81db715514cd532db6b5116a25ea3c03d6780a2" + +[[package]] +name = "elliptic-curve" +version = "0.13.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b5e6043086bf7973472e0c7dff2142ea0b680d30e18d9cc40f267efbf222bd47" +dependencies = [ + "base16ct", + "base64ct", + "crypto-bigint", + "digest", + "ff", + "generic-array", + "group", + "pem-rfc7468", + "pkcs8", + "rand_core", + "sec1", + "serde_json", + "serdect", + "subtle", + "zeroize", +] [[package]] name = "encode_unicode" @@ -1203,9 +1333,9 @@ checksum = "a357d28ed41a50f9c765dbfe56cbc04a64e53e5fc58ba79fbc34c10ef3df831f" [[package]] name = "encoding_rs" -version = "0.8.33" +version = "0.8.34" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7268b386296a025e474d5140678f75d6de9493ae55a5d709eeb9dd08149945e1" +checksum = "b45de904aa0b010bce2ab45264d0631681847fa7b6f2eaa7dab7619943bc4f59" dependencies = [ "cfg-if", ] @@ -1292,15 +1422,25 @@ dependencies = [ [[package]] name = "fastrand" -version = "2.0.1" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9fc0510504f03c51ada170672ac806f1f105a88aa97a5281117e1ddc3368e51a" + +[[package]] +name = "ff" +version = "0.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "25cbce373ec4653f1a01a31e8a5e5ec0c622dc27ff9c4e6606eefef5cbbed4a5" +checksum = "ded41244b729663b1e574f1b4fb731469f69f79c17667b5d776b16cda0479449" +dependencies = [ + "rand_core", + "subtle", +] [[package]] name = "flate2" -version = "1.0.28" +version = "1.0.30" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "46303f565772937ffe1d394a4fac6f411c6013172fadde9dcdb1e147a086940e" +checksum = "5f54427cfd1c7829e2a139fcefea601bf088ebca651d2bf53ebc600eac295dae" dependencies = [ "crc32fast", "miniz_oxide", @@ -1386,7 +1526,7 @@ checksum = "87750cf4b7a4c0625b1529e4c543c2182106e4dedc60a2a6455e00d212c489ac" dependencies = [ "proc-macro2", "quote", - "syn 2.0.53", + "syn 2.0.60", ] [[package]] @@ -1432,9 +1572,9 @@ dependencies = [ [[package]] name = "getrandom" -version = "0.2.12" +version = "0.2.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "190092ea657667030ac6a35e305e62fc4dd69fd98ac98631e5d3a2b1575a12b5" +checksum = "94b22e06ecb0110981051723910cbf0b5f5e09a2062dd7663334ee79a9d1286c" dependencies = [ "cfg-if", "js-sys", @@ -1470,20 +1610,31 @@ dependencies = [ [[package]] name = "goblin" -version = "0.8.0" +version = "0.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bb07a4ffed2093b118a525b1d8f5204ae274faed5604537caf7135d0f18d9887" +checksum = "1b363a30c165f666402fe6a3024d3bec7ebc898f96a4a23bd1c99f8dbf3f4f47" dependencies = [ "log", "plain", "scroll", ] +[[package]] +name = "group" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0f9ef7462f7c099f518d754361858f86d8a07af53ba9af0fe635bbccb151a63" +dependencies = [ + "ff", + "rand_core", + "subtle", +] + [[package]] name = "h2" -version = "0.4.3" +version = "0.4.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "51ee2dd2e4f378392eeff5d51618cd9a63166a2513846bbc55f21cfacd9199d4" +checksum = "816ec7294445779408f36fe57bc5b7fc1cf59664059096c65f905c1c61f58069" dependencies = [ "bytes", "fnv", @@ -1491,13 +1642,23 @@ dependencies = [ "futures-sink", "futures-util", "http", - "indexmap 2.2.5", + "indexmap 2.2.6", "slab", "tokio", "tokio-util", "tracing", ] +[[package]] +name = "half" +version = "2.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6dd08c532ae367adf81c312a4580bc67f1d0fe8bc9c460520283f4c0ff277888" +dependencies = [ + "cfg-if", + "crunchy", +] + [[package]] name = "hashbrown" version = "0.12.3" @@ -1506,9 +1667,9 @@ checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" [[package]] name = "hashbrown" -version = "0.14.3" +version = "0.14.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "290f1a1d9242c78d09ce40a5e87e7554ee637af1351968159f4952f028f75604" +checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1" [[package]] name = "heck" @@ -1615,9 +1776,9 @@ checksum = "9a3a5bfb195931eeb336b2a7b4d761daec841b97f947d34394601737a7bba5e4" [[package]] name = "hyper" -version = "1.2.0" +version = "1.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "186548d73ac615b32a73aafe38fb4f56c0d340e110e5a200bcadbaf2e199263a" +checksum = "fe575dd17d0862a9a33781c8c4696a55c320909004a67a00fb286ba8b1bc496d" dependencies = [ "bytes", "futures-channel", @@ -1729,19 +1890,20 @@ dependencies = [ [[package]] name = "indexmap" -version = "2.2.5" +version = "2.2.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7b0b929d511467233429c45a44ac1dcaa21ba0f5ba11e4879e6ed28ddb4f9df4" +checksum = "168fb715dda47215e360912c096649d23d58bf392ac62f73919e831745e40f26" dependencies = [ "equivalent", - "hashbrown 0.14.3", + "hashbrown 0.14.5", + "serde", ] [[package]] name = "indoc" -version = "2.0.4" +version = "2.0.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e186cfbae8084e513daff4240b4797e342f988cecda4fb6c939150f96315fd8" +checksum = "b248f5224d1d606005e02c97f5aa4e88eeb230488bcc03bc9ca4d7991399f2b5" [[package]] name = "inout" @@ -1801,9 +1963,9 @@ dependencies = [ [[package]] name = "itoa" -version = "1.0.10" +version = "1.0.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b1a46d1a171d865aa5f83f92695765caa047a9b4cbae2cbf37dbd613a793fd4c" +checksum = "49f1f14873335454500d59611f1cf4a4b0f786f9ac11f4312a78e4cf2566695b" [[package]] name = "jni" @@ -1845,9 +2007,9 @@ dependencies = [ [[package]] name = "libc" -version = "0.2.153" +version = "0.2.154" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9c198f91728a82281a64e1f4f9eeb25d82cb32a5de251c6bd1b5154d63a8e7bd" +checksum = "ae743338b92ff9146ce83992f766a31066a91a8c84a45e0e9f21e7cf6de6d346" [[package]] name = "libloading" @@ -1856,7 +2018,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0c2a198fb6b0eada2a8df47933734e6d35d350665a33a3593d7164fa52c75c19" dependencies = [ "cfg-if", - "windows-targets 0.52.4", + "windows-targets 0.48.5", ] [[package]] @@ -1867,23 +2029,19 @@ checksum = "4ec2a862134d2a7d32d7983ddcdd1c4923530833c9f2ea1a44fc5fa473989058" [[package]] name = "libredox" -version = "0.0.1" +version = "0.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "85c833ca1e66078851dba29046874e38f08b2c883700aa29a03ddd3b23814ee8" +checksum = "c0ff37bd590ca25063e35af745c343cb7a0271906fb7b37e4813e8f79f00268d" dependencies = [ "bitflags 2.5.0", "libc", - "redox_syscall", ] [[package]] name = "line-wrap" -version = "0.1.1" +version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f30344350a2a51da54c1d53be93fade8a237e545dbcc4bdbe635413f2117cab9" -dependencies = [ - "safemem", -] +checksum = "dd1bc4d24ad230d21fb898d1116b1801d7adfc449d42026475862ab48b11e70e" [[package]] name = "linux-raw-sys" @@ -1893,9 +2051,9 @@ checksum = "01cda141df6706de531b6c46c3a33ecca755538219bd484262fa09410c13539c" [[package]] name = "lock_api" -version = "0.4.11" +version = "0.4.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3c168f8615b12bc01f9c17e2eb0cc07dcae1940121185446edc3744920e8ef45" +checksum = "07af8b9cdd281b7915f413fa73f29ebd5d55d0d3f0155584dade1ff18cea1b17" dependencies = [ "autocfg", "scopeguard", @@ -1909,15 +2067,15 @@ checksum = "90ed8c1e510134f979dbc4f070f87d4313098b704861a105fe34231c70a3901c" [[package]] name = "memchr" -version = "2.7.1" +version = "2.7.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "523dc4f511e55ab87b694dc30d0f820d60906ef06413f93d4d7a1385599cc149" +checksum = "6c8640c5d730cb13ebd907d8d04b52f55ac9a2eec55b440c8892f40d56c76c1d" [[package]] name = "memoffset" -version = "0.9.0" +version = "0.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5a634b1c61a95585bd15607c6ab0c4e5b226e695ff2800ba0cdccddf208c406c" +checksum = "488016bfae457b036d996092f6cb448677611ce4449e970ceaf42695203f218a" dependencies = [ "autocfg", ] @@ -1979,9 +2137,9 @@ dependencies = [ [[package]] name = "napi" -version = "2.16.0" +version = "2.16.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "54a63d0570e4c3e0daf7a8d380563610e159f538e20448d6c911337246f40e84" +checksum = "da1edd9510299935e4f52a24d1e69ebd224157e3e962c6c847edec5c2e4f786f" dependencies = [ "bitflags 2.5.0", "ctor", @@ -1993,29 +2151,29 @@ dependencies = [ [[package]] name = "napi-build" -version = "2.1.2" +version = "2.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2f9130fccc5f763cf2069b34a089a18f0d0883c66aceb81f2fad541a3d823c43" +checksum = "e1c0f5d67ee408a4685b61f5ab7e58605c8ae3f2b4189f0127d804ff13d5560a" [[package]] name = "napi-derive" -version = "2.16.0" +version = "2.16.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "05bb7c37e3c1dda9312fdbe4a9fc7507fca72288ba154ec093e2d49114e727ce" +checksum = "e5a6de411b6217dbb47cd7a8c48684b162309ff48a77df9228c082400dd5b030" dependencies = [ "cfg-if", "convert_case", "napi-derive-backend", "proc-macro2", "quote", - "syn 2.0.53", + "syn 2.0.60", ] [[package]] name = "napi-derive-backend" -version = "1.0.62" +version = "1.0.65" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f785a8b8d7b83e925f5aa6d2ae3c159d17fe137ac368dc185bef410e7acdaeb4" +checksum = "c3e35868d43b178b0eb9c17bd018960b1b5dd1732a7d47c23debe8f5c4caf498" dependencies = [ "convert_case", "once_cell", @@ -2023,14 +2181,14 @@ dependencies = [ "quote", "regex", "semver", - "syn 2.0.53", + "syn 2.0.60", ] [[package]] name = "napi-sys" -version = "2.3.0" +version = "2.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2503fa6af34dc83fb74888df8b22afe933b58d37daf7d80424b1c60c68196b8b" +checksum = "427802e8ec3a734331fec1035594a210ce1ff4dc5bc1950530920ab717964ea3" dependencies = [ "libloading", ] @@ -2198,6 +2356,18 @@ version = "3.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c1b04fb49957986fdce4d6ee7a65027d55d4b6d2265e5848bbb507b58ccfdb6f" +[[package]] +name = "p256" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c9863ad85fa8f4460f9c48cb909d38a0d689dba1f6f6988a5e3e0d31071bcd4b" +dependencies = [ + "ecdsa", + "elliptic-curve", + "primeorder", + "sha2", +] + [[package]] name = "parking" version = "2.2.0" @@ -2206,9 +2376,9 @@ checksum = "bb813b8af86854136c6922af0598d719255ecb2179515e6e7730d468f05c9cae" [[package]] name = "parking_lot" -version = "0.12.1" +version = "0.12.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3742b2c103b9f06bc9fff0a37ff4912935851bee6d36f3c02bcc755bcfec228f" +checksum = "7e4af0ca4f6caed20e900d564c242b8e5d4903fdacf31d3daf527b66fe6f42fb" dependencies = [ "lock_api", "parking_lot_core", @@ -2216,15 +2386,80 @@ dependencies = [ [[package]] name = "parking_lot_core" -version = "0.9.9" +version = "0.9.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4c42a9226546d68acdd9c0a280d17ce19bfe27a46bf68784e4066115788d008e" +checksum = "1e401f977ab385c9e4e3ab30627d6f26d00e2c73eef317493c4ec6d468726cf8" dependencies = [ "cfg-if", "libc", "redox_syscall", "smallvec", - "windows-targets 0.48.5", + "windows-targets 0.52.5", +] + +[[package]] +name = "passkey" +version = "0.3.1" +source = "git+https://github.com/bitwarden/passkey-rs?rev=12da886102707f87ad97e499c857c0857ece0b85#12da886102707f87ad97e499c857c0857ece0b85" +dependencies = [ + "passkey-authenticator", + "passkey-client", + "passkey-transports", + "passkey-types", +] + +[[package]] +name = "passkey-authenticator" +version = "0.3.0" +source = "git+https://github.com/bitwarden/passkey-rs?rev=12da886102707f87ad97e499c857c0857ece0b85#12da886102707f87ad97e499c857c0857ece0b85" +dependencies = [ + "async-trait", + "coset", + "log", + "p256", + "passkey-types", + "rand", +] + +[[package]] +name = "passkey-client" +version = "0.3.1" +source = "git+https://github.com/bitwarden/passkey-rs?rev=12da886102707f87ad97e499c857c0857ece0b85#12da886102707f87ad97e499c857c0857ece0b85" +dependencies = [ + "ciborium", + "coset", + "idna", + "passkey-authenticator", + "passkey-types", + "public-suffix", + "serde", + "serde_json", + "typeshare", + "url", +] + +[[package]] +name = "passkey-transports" +version = "0.1.0" +source = "git+https://github.com/bitwarden/passkey-rs?rev=12da886102707f87ad97e499c857c0857ece0b85#12da886102707f87ad97e499c857c0857ece0b85" + +[[package]] +name = "passkey-types" +version = "0.2.1" +source = "git+https://github.com/bitwarden/passkey-rs?rev=12da886102707f87ad97e499c857c0857ece0b85#12da886102707f87ad97e499c857c0857ece0b85" +dependencies = [ + "bitflags 2.5.0", + "ciborium", + "coset", + "data-encoding", + "getrandom", + "indexmap 2.2.6", + "rand", + "serde", + "serde_json", + "sha2", + "strum 0.25.0", + "typeshare", ] [[package]] @@ -2294,14 +2529,14 @@ checksum = "2f38a4412a78282e09a2cf38d195ea5420d15ba0602cb375210efbc877243965" dependencies = [ "proc-macro2", "quote", - "syn 2.0.53", + "syn 2.0.60", ] [[package]] name = "pin-project-lite" -version = "0.2.13" +version = "0.2.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8afb450f006bf6385ca15ef45d71d2288452bc3683ce2e2cacc0d18e4be60b58" +checksum = "bda66fc9667c18cb2758a2ac84d1167245054bcf85d5d1aaa6923f45801bdd02" [[package]] name = "pin-utils" @@ -2344,12 +2579,12 @@ checksum = "b4596b6d070b27117e987119b4dac604f3c58cfb0b191112e24771b2faeac1a6" [[package]] name = "plist" -version = "1.6.0" +version = "1.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e5699cc8a63d1aa2b1ee8e12b9ad70ac790d65788cd36101fa37f87ea46c4cef" +checksum = "d9d34169e64b3c7a80c8621a48adaf44e0cf62c78a9b25dd9dd35f1881a17cf9" dependencies = [ - "base64", - "indexmap 2.2.5", + "base64 0.21.7", + "indexmap 2.2.6", "line-wrap", "quick-xml", "serde", @@ -2374,15 +2609,29 @@ version = "0.2.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5b40af805b3121feab8a3c29f04d8ad262fa8e0561883e7653e024ae4479e6de" +[[package]] +name = "primeorder" +version = "0.13.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "353e1ca18966c16d9deb1c69278edbc5f194139612772bd9537af60ac231e1e6" +dependencies = [ + "elliptic-curve", +] + [[package]] name = "proc-macro2" -version = "1.0.79" +version = "1.0.81" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e835ff2298f5721608eb1a980ecaee1aef2c132bf95ecc026a11b7bf3c01c02e" +checksum = "3d1597b0c024618f09a9c3b8655b7e430397a36d23fdafec26d6965e9eec3eba" dependencies = [ "unicode-ident", ] +[[package]] +name = "public-suffix" +version = "0.1.1" +source = "git+https://github.com/bitwarden/passkey-rs?rev=12da886102707f87ad97e499c857c0857ece0b85#12da886102707f87ad97e499c857c0857ece0b85" + [[package]] name = "pyo3" version = "0.20.3" @@ -2466,7 +2715,7 @@ dependencies = [ "proc-macro2", "pyo3-macros-backend", "quote", - "syn 2.0.53", + "syn 2.0.60", ] [[package]] @@ -2479,7 +2728,7 @@ dependencies = [ "proc-macro2", "pyo3-build-config", "quote", - "syn 2.0.53", + "syn 2.0.60", ] [[package]] @@ -2499,9 +2748,9 @@ dependencies = [ [[package]] name = "quote" -version = "1.0.35" +version = "1.0.36" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "291ec9ab5efd934aaf503a6466c5d5251535d108ee747472c3977cc5acc868ef" +checksum = "0fa76aaf39101c457836aec0ce2316dbdc3ab723cdda1c6bd4e6ad4208acaca7" dependencies = [ "proc-macro2", ] @@ -2538,9 +2787,9 @@ dependencies = [ [[package]] name = "rayon" -version = "1.9.0" +version = "1.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e4963ed1bc86e4f3ee217022bd855b297cef07fb9eac5dfa1f788b220b49b3bd" +checksum = "b418a60154510ca1a002a752ca9714984e21e4241e804d32555251faf8b78ffa" dependencies = [ "either", "rayon-core", @@ -2558,18 +2807,18 @@ dependencies = [ [[package]] name = "redox_syscall" -version = "0.4.1" +version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4722d768eff46b75989dd134e5c353f0d6296e5aaa3132e776cbdb56be7731aa" +checksum = "469052894dcb553421e483e4209ee581a45100d31b4018de03e5a7ad86374a7e" dependencies = [ - "bitflags 1.3.2", + "bitflags 2.5.0", ] [[package]] name = "redox_users" -version = "0.4.4" +version = "0.4.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a18479200779601e498ada4e8c1e1f50e3ee19deb0259c25825a98b5603b2cb4" +checksum = "bd283d9651eeda4b2a83a43c1c91b266c40fd76ecd39a50a8c630ae69dc72891" dependencies = [ "getrandom", "libredox", @@ -2578,9 +2827,9 @@ dependencies = [ [[package]] name = "regex" -version = "1.10.3" +version = "1.10.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b62dbe01f0b06f9d8dc7d49e05a0785f153b00b2c227856282f671e0318c9b15" +checksum = "c117dbdfde9c8308975b6a18d71f3f385c89461f7b3fb054288ecf2a2058ba4c" dependencies = [ "aho-corasick", "memchr", @@ -2601,20 +2850,21 @@ dependencies = [ [[package]] name = "regex-syntax" -version = "0.8.2" +version = "0.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c08c74e62047bb2de4ff487b251e4a92e24f48745648451635cec7d591162d9f" +checksum = "adad44e29e4c806119491a7f06f03de4d1af22c3a680dd47f1e6e179439d1f56" [[package]] name = "reqwest" -version = "0.12.0" +version = "0.12.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "58b48d98d932f4ee75e541614d32a7f44c889b72bd9c2e04d95edd135989df88" +checksum = "566cafdd92868e0939d3fb961bd0dc25fcfaaed179291093b3d43e6b3150ea10" dependencies = [ - "base64", + "base64 0.22.1", "bytes", "futures-core", "futures-util", + "h2", "http", "http-body", "http-body-util", @@ -2630,7 +2880,7 @@ dependencies = [ "percent-encoding", "pin-project-lite", "rustls", - "rustls-pemfile 1.0.4", + "rustls-pemfile", "rustls-pki-types", "serde", "serde_json", @@ -2647,6 +2897,16 @@ dependencies = [ "winreg", ] +[[package]] +name = "rfc6979" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8dd2a808d456c4a54e300a23e9f5a67e122c3024119acbfd73e3bf664491cb2" +dependencies = [ + "hmac", + "subtle", +] + [[package]] name = "rgb" version = "0.8.37" @@ -2671,6 +2931,12 @@ dependencies = [ "windows-sys 0.52.0", ] +[[package]] +name = "roff" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b833d8d034ea094b1ea68aa6d5c740e0d04bad9d16568d08ba6f76823a114316" + [[package]] name = "rsa" version = "0.9.6" @@ -2699,9 +2965,9 @@ checksum = "d626bb9dae77e28219937af045c257c28bfd3f69333c512553507f5f9798cb76" [[package]] name = "rustix" -version = "0.38.32" +version = "0.38.34" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "65e04861e65f21776e67888bfbea442b3642beaa0138fdb1dd7a84a52dffdb89" +checksum = "70dc5ec042f7a43c4a73241207cecc9873a06d45debb38b329f8541d85c2730f" dependencies = [ "bitflags 2.5.0", "errno", @@ -2712,9 +2978,9 @@ dependencies = [ [[package]] name = "rustls" -version = "0.22.2" +version = "0.22.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e87c9956bd9807afa1f77e0f7594af32566e830e088a5576d27c5b6f30f49d41" +checksum = "bf4ef73721ac7bcd79b2b315da7779d8fc09718c6b3d2d1b2d94850eb8c18432" dependencies = [ "log", "ring", @@ -2731,7 +2997,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8f1fb85efa936c42c6d5fc28d2629bb51e4b2f4b8a5211e297d599cc5a093792" dependencies = [ "openssl-probe", - "rustls-pemfile 2.1.1", + "rustls-pemfile", "rustls-pki-types", "schannel", "security-framework", @@ -2739,28 +3005,19 @@ dependencies = [ [[package]] name = "rustls-pemfile" -version = "1.0.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1c74cae0a4cf6ccbbf5f359f08efdf8ee7e1dc532573bf0db71968cb56b1448c" -dependencies = [ - "base64", -] - -[[package]] -name = "rustls-pemfile" -version = "2.1.1" +version = "2.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f48172685e6ff52a556baa527774f61fcaa884f59daf3375c62a3f1cd2549dab" +checksum = "29993a25686778eb88d4189742cd713c9bce943bc54251a33509dc63cbacf73d" dependencies = [ - "base64", + "base64 0.22.1", "rustls-pki-types", ] [[package]] name = "rustls-pki-types" -version = "1.3.1" +version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5ede67b28608b4c60685c7d54122d4400d90f62b40caee7700e700380a390fa8" +checksum = "beb461507cee2c2ff151784c52762cf4d9ff6a61f3e80968600ed24fa837fa54" [[package]] name = "rustls-platform-verifier" @@ -2791,9 +3048,9 @@ checksum = "84e217e7fdc8466b5b35d30f8c0a30febd29173df4a3a0c2115d306b9c4117ad" [[package]] name = "rustls-webpki" -version = "0.102.2" +version = "0.102.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "faaa0a62740bedb9b2ef5afa303da42764c012f743917351dc9a237ea1663610" +checksum = "f3bce581c0dd41bce533ce695a1437fa16a7ab5ac3ccfa99fe1a620a7885eabf" dependencies = [ "ring", "rustls-pki-types", @@ -2802,9 +3059,9 @@ dependencies = [ [[package]] name = "rustversion" -version = "1.0.14" +version = "1.0.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7ffc183a10b4478d04cbbbfc96d0873219d962dd5accaff2ffbd4ceb7df837f4" +checksum = "80af6f9131f277a45a3fba6ce8e2258037bb0477a67e610d3c1fe046ab31de47" [[package]] name = "ryu" @@ -2812,12 +3069,6 @@ version = "1.0.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e86697c916019a8588c99b5fac3cead74ec0b4b819707a682fd4d23fa0ce1ba1" -[[package]] -name = "safemem" -version = "0.3.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ef703b7cb59335eae2eb93ceb664c0eb7ea6bf567079d843e09420219668e072" - [[package]] name = "same-file" version = "1.0.6" @@ -2838,9 +3089,9 @@ dependencies = [ [[package]] name = "schemars" -version = "0.8.16" +version = "0.8.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "45a28f4c49489add4ce10783f7911893516f15afe45d015608d41faca6bc4d29" +checksum = "7f55c82c700538496bdc329bb4918a81f87cc8888811bd123cf325a0f2f8d309" dependencies = [ "chrono", "dyn-clone", @@ -2853,14 +3104,14 @@ dependencies = [ [[package]] name = "schemars_derive" -version = "0.8.16" +version = "0.8.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c767fd6fa65d9ccf9cf026122c1b555f2ef9a4f0cea69da4d7dbc3e258d30967" +checksum = "83263746fe5e32097f06356968a077f96089739c927a61450efa069905eec108" dependencies = [ "proc-macro2", "quote", "serde_derive_internals", - "syn 1.0.109", + "syn 2.0.60", ] [[package]] @@ -2892,7 +3143,7 @@ checksum = "7f81c2fde025af7e69b1d1420531c8a8811ca898919db177141a85313b1cb932" dependencies = [ "proc-macro2", "quote", - "syn 2.0.53", + "syn 2.0.60", ] [[package]] @@ -2908,11 +3159,26 @@ dependencies = [ "serde_json", ] +[[package]] +name = "sec1" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3e97a565f76233a6003f9f5c54be1d9c5bdfa3eccfb189469f11ec4901c47dc" +dependencies = [ + "base16ct", + "der", + "generic-array", + "pkcs8", + "serdect", + "subtle", + "zeroize", +] + [[package]] name = "security-framework" -version = "2.9.2" +version = "2.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "05b64fb303737d99b81884b2c63433e9ae28abebe5eb5045dcdd175dc2ecf4de" +checksum = "770452e37cad93e0a50d5abc3990d2bc351c36d0328f86cefec2f2fb206eaef6" dependencies = [ "bitflags 1.3.2", "core-foundation", @@ -2924,9 +3190,9 @@ dependencies = [ [[package]] name = "security-framework-sys" -version = "2.9.1" +version = "2.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e932934257d3b408ed8f30db49d85ea163bfe74961f017f405b025af298f0c7a" +checksum = "41f3cc463c0ef97e11c3461a9d3787412d30e8e7eb907c79180c4a57bf7c04ef" dependencies = [ "core-foundation-sys", "libc", @@ -2943,41 +3209,42 @@ dependencies = [ [[package]] name = "serde" -version = "1.0.197" +version = "1.0.200" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3fb1c873e1b9b056a4dc4c0c198b24c3ffa059243875552b2bd0933b1aee4ce2" +checksum = "ddc6f9cc94d67c0e21aaf7eda3a010fd3af78ebf6e096aa6e2e13c79749cce4f" dependencies = [ "serde_derive", ] [[package]] name = "serde_derive" -version = "1.0.197" +version = "1.0.200" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7eb0b34b42edc17f6b7cac84a52a1c5f0e1bb2227e997ca9011ea3dd34e8610b" +checksum = "856f046b9400cee3c8c94ed572ecdb752444c24528c035cd35882aad6f492bcb" dependencies = [ "proc-macro2", "quote", - "syn 2.0.53", + "syn 2.0.60", ] [[package]] name = "serde_derive_internals" -version = "0.26.0" +version = "0.29.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "85bf8229e7920a9f636479437026331ce11aa132b4dde37d121944a44d6e5f3c" +checksum = "330f01ce65a3a5fe59a60c82f3c9a024b573b8a6e875bd233fe5f934e71d54e3" dependencies = [ "proc-macro2", "quote", - "syn 1.0.109", + "syn 2.0.60", ] [[package]] name = "serde_json" -version = "1.0.114" +version = "1.0.116" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c5f09b1bd632ef549eaa9f60a1f8de742bdbc698e6cee2095fc84dde5f549ae0" +checksum = "3e17db7126d17feb94eb3fad46bf1a96b034e8aacbc2e775fe81505f8b0b2813" dependencies = [ + "indexmap 2.2.6", "itoa", "ryu", "serde", @@ -2985,9 +3252,9 @@ dependencies = [ [[package]] name = "serde_qs" -version = "0.12.0" +version = "0.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0431a35568651e363364210c91983c1da5eb29404d9f0928b67d4ebcfa7d330c" +checksum = "cd34f36fe4c5ba9654417139a9b3a20d2e1de6012ee678ad14d240c22c78d8d6" dependencies = [ "percent-encoding", "serde", @@ -2996,13 +3263,13 @@ dependencies = [ [[package]] name = "serde_repr" -version = "0.1.18" +version = "0.1.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b2e6b945e9d3df726b65d6ee24060aff8e3533d431f677a9695db04eff9dfdb" +checksum = "6c64451ba24fc7a6a2d60fc75dd9c83c90903b19028d4eff35e88fc1e86564e9" dependencies = [ "proc-macro2", "quote", - "syn 2.0.53", + "syn 2.0.60", ] [[package]] @@ -3028,17 +3295,27 @@ dependencies = [ [[package]] name = "serde_yaml" -version = "0.9.33" +version = "0.9.34+deprecated" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a0623d197252096520c6f2a5e1171ee436e5af99a5d7caa2891e55e61950e6d9" +checksum = "6a8b1a1a2ebf674015cc02edccce75287f1a0130d394307b36743c2f5d504b47" dependencies = [ - "indexmap 2.2.5", + "indexmap 2.2.6", "itoa", "ryu", "serde", "unsafe-libyaml", ] +[[package]] +name = "serdect" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a84f14a19e9a014bb9f4512488d9829a68e04ecabffb0f9904cd1ace94598177" +dependencies = [ + "base16ct", + "serde", +] + [[package]] name = "sha1" version = "0.10.6" @@ -3093,9 +3370,9 @@ dependencies = [ [[package]] name = "signal-hook-registry" -version = "1.4.1" +version = "1.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d8229b473baa5980ac72ef434c4415e70c4b5e71b423043adb4ba059f89c99a1" +checksum = "a9e9e0b4211b72e7b8b6e85c807d36c212bdb33ea8587f7569562a84df5465b1" dependencies = [ "libc", ] @@ -3139,9 +3416,9 @@ checksum = "b7c388c1b5e93756d0c740965c41e8822f866621d41acbdf6336a6a168f8840c" [[package]] name = "socket2" -version = "0.5.6" +version = "0.5.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "05ffd9c0a93b7543e062e759284fcf5f5e3b098501104bfbdde4d404db792871" +checksum = "ce305eb0b4296696835b71df73eb912e0f1ffd2556a501fcede6e0c50349191c" dependencies = [ "libc", "windows-sys 0.52.0", @@ -3189,15 +3466,24 @@ checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623" [[package]] name = "strsim" -version = "0.11.0" +version = "0.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5ee073c9e4cd00e28217186dbe12796d692868f432bf2e97ee73bed0c56dfa01" +checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" [[package]] name = "strum" version = "0.25.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "290d54ea6f91c969195bdbcd7442c8c2a2ba87da8bf60a7ee86a235d4bc1e125" +dependencies = [ + "strum_macros 0.25.3", +] + +[[package]] +name = "strum" +version = "0.26.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d8cec3501a5194c432b2b7976db6b7d10ec95c253208b45f83f7136aa985e29" [[package]] name = "strum_macros" @@ -3209,7 +3495,20 @@ dependencies = [ "proc-macro2", "quote", "rustversion", - "syn 2.0.53", + "syn 2.0.60", +] + +[[package]] +name = "strum_macros" +version = "0.26.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c6cf59daf282c0a494ba14fd21610a0325f9f90ec9d1231dea26bcb1d696c946" +dependencies = [ + "heck 0.4.1", + "proc-macro2", + "quote", + "rustversion", + "syn 2.0.60", ] [[package]] @@ -3240,9 +3539,9 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.53" +version = "2.0.60" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7383cd0e49fff4b6b90ca5670bfd3e9d6a733b3f90c686605aa7eec8c4996032" +checksum = "909518bc7b1c9b779f1bbf07f2929d35af9f0f37e47c6e9ef7f9dddc1e1821f3" dependencies = [ "proc-macro2", "quote", @@ -3306,22 +3605,22 @@ dependencies = [ [[package]] name = "thiserror" -version = "1.0.58" +version = "1.0.59" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "03468839009160513471e86a034bb2c5c0e4baae3b43f79ffc55c4a5427b3297" +checksum = "f0126ad08bff79f29fc3ae6a55cc72352056dfff61e3ff8bb7129476d44b23aa" dependencies = [ "thiserror-impl", ] [[package]] name = "thiserror-impl" -version = "1.0.58" +version = "1.0.59" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c61f3ba182994efc43764a46c018c347bc492c79f024e705f46567b418f6d4f7" +checksum = "d1cd413b5d558b4c5bf3680e324a6fa5014e7b7c067a51e69dbdf47eb7148b66" dependencies = [ "proc-macro2", "quote", - "syn 2.0.53", + "syn 2.0.60", ] [[package]] @@ -3336,9 +3635,9 @@ dependencies = [ [[package]] name = "time" -version = "0.3.34" +version = "0.3.36" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c8248b6521bb14bc45b4067159b9b6ad792e2d6d754d6c41fb50e29fefe38749" +checksum = "5dfd88e563464686c916c7e46e623e520ddc6d79fa6641390f2e3fa86e83e885" dependencies = [ "deranged", "itoa", @@ -3357,9 +3656,9 @@ checksum = "ef927ca75afb808a4d64dd374f00a2adf8d0fcff8e7b184af886c3c87ec4a3f3" [[package]] name = "time-macros" -version = "0.2.17" +version = "0.2.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7ba3a3ef41e6672a2f0f001392bb5dcd3ff0a9992d618ca761a11c3121547774" +checksum = "3f252a68540fde3a3877aeea552b832b40ab9a69e318efd078774a01ddee1ccf" dependencies = [ "num-conv", "time-core", @@ -3382,9 +3681,9 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" [[package]] name = "tokio" -version = "1.36.0" +version = "1.37.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "61285f6515fa018fb2d1e46eb21223fff441ee8db5d0f1435e8ab4f5cdb80931" +checksum = "1adbebffeca75fcfd058afa480fb6c0b81e165a0323f9c9d39c9697e37c46787" dependencies = [ "backtrace", "bytes", @@ -3405,7 +3704,7 @@ checksum = "5b8a1e28f2deaa14e508979454cb3a223b10b938b45af148bc0986de36f1923b" dependencies = [ "proc-macro2", "quote", - "syn 2.0.53", + "syn 2.0.60", ] [[package]] @@ -3465,11 +3764,11 @@ dependencies = [ [[package]] name = "toml_edit" -version = "0.22.9" +version = "0.22.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e40bb779c5187258fd7aad0eb68cb8706a0a81fa712fbea808ab43c4b8374c4" +checksum = "d3328d4f68a705b2a4498da1d580585d39a6510f98318a2cec3018a7ec61ddef" dependencies = [ - "indexmap 2.2.5", + "indexmap 2.2.6", "serde", "serde_spanned", "toml_datetime", @@ -3558,6 +3857,28 @@ version = "1.17.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "42ff0bf0c66b8238c6f3b578df37d0b7848e55df8577b3f74f92a69acceeb825" +[[package]] +name = "typeshare" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "04f17399b76c2e743d58eac0635d7686e9c00f48cd4776f00695d9882a7d3187" +dependencies = [ + "chrono", + "serde", + "serde_json", + "typeshare-annotation", +] + +[[package]] +name = "typeshare-annotation" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a615d6c2764852a2e88a4f16e9ce1ea49bb776b5872956309e170d63a042a34f" +dependencies = [ + "quote", + "syn 2.0.60", +] + [[package]] name = "unicase" version = "2.7.0" @@ -3602,15 +3923,15 @@ checksum = "d4c87d22b6e3f4a18d4d40ef354e97c90fcb14dd91d7dc0aa9d8a1172ebf7202" [[package]] name = "unicode-width" -version = "0.1.11" +version = "0.1.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e51733f11c9c4f72aa0c160008246859e340b00807569a0da0e7a1079b27ba85" +checksum = "68f5e5f3158ecfd4b8ff6fe086db7c8467a2dfdac97fe420f2b7c4aa97af66d6" [[package]] name = "uniffi" -version = "0.26.1" +version = "0.27.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5ad0be8bba6c242d2d16922de4a9c8f167b9491729fda552e70f8626bf7302cb" +checksum = "a5566fae48a5cb017005bf9cd622af5236b2a203a13fb548afde3506d3c68277" dependencies = [ "anyhow", "camino", @@ -3630,9 +3951,9 @@ dependencies = [ [[package]] name = "uniffi_bindgen" -version = "0.26.1" +version = "0.27.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ab31006ab9c9c6870739f0e74235729d1478d82e73571b8f53c25aa176d67535" +checksum = "4a77bb514bcd4bf27c9bd404d7c3f2a6a8131b957eba9c22cfeb7751c4278e09" dependencies = [ "anyhow", "askama", @@ -3655,9 +3976,9 @@ dependencies = [ [[package]] name = "uniffi_build" -version = "0.26.1" +version = "0.27.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4aa3a7608c6872dc1ce53199d816a24d2e19af952d82ce557ecc8692a4ae9cba" +checksum = "45cba427aeb7b3a8b54830c4c915079a7a3c62608dd03dddba1d867a8a023eb4" dependencies = [ "anyhow", "camino", @@ -3666,19 +3987,19 @@ dependencies = [ [[package]] name = "uniffi_checksum_derive" -version = "0.26.1" +version = "0.27.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "72775b3afa6adb30e0c92b3107858d2fcb0ff1a417ac242db1f648b0e2dd0ef2" +checksum = "ae7e5a6c33b1dec3f255f57ec0b6af0f0b2bb3021868be1d5eec7a38e2905ebc" dependencies = [ "quote", - "syn 2.0.53", + "syn 2.0.60", ] [[package]] name = "uniffi_core" -version = "0.26.1" +version = "0.27.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8d6e8db3f4e558faf0e25ac4b5bd775567973a4e18809f1123e74de52a853692" +checksum = "0ea3eb5474d50fc149b7e4d86b9c5bd4a61dcc167f0683902bf18ae7bbb3deef" dependencies = [ "anyhow", "async-compat", @@ -3693,9 +4014,9 @@ dependencies = [ [[package]] name = "uniffi_macros" -version = "0.26.1" +version = "0.27.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a126650799f97d97d8e38e3f10f15c65f5bc5a76b021bec21823efe9dd831a02" +checksum = "18331d35003f46f0d04047fbe4227291815b83a937a8c32bc057f990962182c4" dependencies = [ "bincode", "camino", @@ -3704,17 +4025,16 @@ dependencies = [ "proc-macro2", "quote", "serde", - "syn 2.0.53", + "syn 2.0.60", "toml 0.5.11", - "uniffi_build", "uniffi_meta", ] [[package]] name = "uniffi_meta" -version = "0.26.1" +version = "0.27.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3f64a99e905671738d9d293f9cce58708ce1af8e13ea29f9d6b6925114fc2e85" +checksum = "f7224422c4cfd181c7ca9fca2154abca4d21db962f926f270f996edd38b0c4b8" dependencies = [ "anyhow", "bytes", @@ -3724,9 +4044,9 @@ dependencies = [ [[package]] name = "uniffi_testing" -version = "0.26.1" +version = "0.27.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cdca5719a22edf34c8239cc6ac9e3906d7ebc2a3e8a5e6ece4c3dffc312a4251" +checksum = "f8ce878d0bdfc288b58797044eaaedf748526c56eef3575380bb4d4b19d69eee" dependencies = [ "anyhow", "camino", @@ -3737,9 +4057,9 @@ dependencies = [ [[package]] name = "uniffi_udl" -version = "0.26.1" +version = "0.27.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3f6817c15714acccd0d0459f99b524cabebfdd622376464a2c6466a6485bdb4b" +checksum = "8c43c9ed40a8d20a5c3eae2d23031092db6b96dc8e571beb449ba9757484cea0" dependencies = [ "anyhow", "textwrap", @@ -3853,7 +4173,7 @@ dependencies = [ "once_cell", "proc-macro2", "quote", - "syn 2.0.53", + "syn 2.0.60", "wasm-bindgen-shared", ] @@ -3887,7 +4207,7 @@ checksum = "e94f17b526d0a461a191c78ea52bbce64071ed5c04c9ffe424dcb38f74171bb7" dependencies = [ "proc-macro2", "quote", - "syn 2.0.53", + "syn 2.0.60", "wasm-bindgen-backend", "wasm-bindgen-shared", ] @@ -3920,7 +4240,7 @@ checksum = "b7f89739351a2e03cb94beb799d47fb2cac01759b40ec441f7de39b00cbf7ef0" dependencies = [ "proc-macro2", "quote", - "syn 2.0.53", + "syn 2.0.60", ] [[package]] @@ -3969,11 +4289,11 @@ checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" [[package]] name = "winapi-util" -version = "0.1.6" +version = "0.1.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f29e6f9198ba0d26b4c9f07dbe6f9ed633e1f3d5b8b414090084349e46a52596" +checksum = "4d4cc384e1e73b93bafa6fb4f1df8c41695c8a91cf9c4c64358067d15a7b6c6b" dependencies = [ - "winapi", + "windows-sys 0.52.0", ] [[package]] @@ -3988,7 +4308,7 @@ version = "0.52.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "33ab640c8d7e35bf8ba19b884ba838ceb4fba93a4e8c65a9059d08afcfc683d9" dependencies = [ - "windows-targets 0.52.4", + "windows-targets 0.52.5", ] [[package]] @@ -4006,7 +4326,7 @@ version = "0.52.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" dependencies = [ - "windows-targets 0.52.4", + "windows-targets 0.52.5", ] [[package]] @@ -4026,17 +4346,18 @@ dependencies = [ [[package]] name = "windows-targets" -version = "0.52.4" +version = "0.52.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7dd37b7e5ab9018759f893a1952c9420d060016fc19a472b4bb20d1bdd694d1b" +checksum = "6f0713a46559409d202e70e28227288446bf7841d3211583a4b53e3f6d96e7eb" dependencies = [ - "windows_aarch64_gnullvm 0.52.4", - "windows_aarch64_msvc 0.52.4", - "windows_i686_gnu 0.52.4", - "windows_i686_msvc 0.52.4", - "windows_x86_64_gnu 0.52.4", - "windows_x86_64_gnullvm 0.52.4", - "windows_x86_64_msvc 0.52.4", + "windows_aarch64_gnullvm 0.52.5", + "windows_aarch64_msvc 0.52.5", + "windows_i686_gnu 0.52.5", + "windows_i686_gnullvm", + "windows_i686_msvc 0.52.5", + "windows_x86_64_gnu 0.52.5", + "windows_x86_64_gnullvm 0.52.5", + "windows_x86_64_msvc 0.52.5", ] [[package]] @@ -4047,9 +4368,9 @@ checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" [[package]] name = "windows_aarch64_gnullvm" -version = "0.52.4" +version = "0.52.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bcf46cf4c365c6f2d1cc93ce535f2c8b244591df96ceee75d8e83deb70a9cac9" +checksum = "7088eed71e8b8dda258ecc8bac5fb1153c5cffaf2578fc8ff5d61e23578d3263" [[package]] name = "windows_aarch64_msvc" @@ -4059,9 +4380,9 @@ checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" [[package]] name = "windows_aarch64_msvc" -version = "0.52.4" +version = "0.52.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "da9f259dd3bcf6990b55bffd094c4f7235817ba4ceebde8e6d11cd0c5633b675" +checksum = "9985fd1504e250c615ca5f281c3f7a6da76213ebd5ccc9561496568a2752afb6" [[package]] name = "windows_i686_gnu" @@ -4071,9 +4392,15 @@ checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" [[package]] name = "windows_i686_gnu" -version = "0.52.4" +version = "0.52.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "88ba073cf16d5372720ec942a8ccbf61626074c6d4dd2e745299726ce8b89670" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.52.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b474d8268f99e0995f25b9f095bc7434632601028cf86590aea5c8a5cb7801d3" +checksum = "87f4261229030a858f36b459e748ae97545d6f1ec60e5e0d6a3d32e0dc232ee9" [[package]] name = "windows_i686_msvc" @@ -4083,9 +4410,9 @@ checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" [[package]] name = "windows_i686_msvc" -version = "0.52.4" +version = "0.52.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1515e9a29e5bed743cb4415a9ecf5dfca648ce85ee42e15873c3cd8610ff8e02" +checksum = "db3c2bf3d13d5b658be73463284eaf12830ac9a26a90c717b7f771dfe97487bf" [[package]] name = "windows_x86_64_gnu" @@ -4095,9 +4422,9 @@ checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" [[package]] name = "windows_x86_64_gnu" -version = "0.52.4" +version = "0.52.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5eee091590e89cc02ad514ffe3ead9eb6b660aedca2183455434b93546371a03" +checksum = "4e4246f76bdeff09eb48875a0fd3e2af6aada79d409d33011886d3e1581517d9" [[package]] name = "windows_x86_64_gnullvm" @@ -4107,9 +4434,9 @@ checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" [[package]] name = "windows_x86_64_gnullvm" -version = "0.52.4" +version = "0.52.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "77ca79f2451b49fa9e2af39f0747fe999fcda4f5e241b2898624dca97a1f2177" +checksum = "852298e482cd67c356ddd9570386e2862b5673c85bd5f88df9ab6802b334c596" [[package]] name = "windows_x86_64_msvc" @@ -4119,24 +4446,24 @@ checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" [[package]] name = "windows_x86_64_msvc" -version = "0.52.4" +version = "0.52.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32b752e52a2da0ddfbdbcc6fceadfeede4c939ed16d13e648833a61dfb611ed8" +checksum = "bec47e5bfd1bff0eeaf6d8b485cc1074891a197ab4225d504cb7a1ab88b02bf0" [[package]] name = "winnow" -version = "0.6.5" +version = "0.6.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dffa400e67ed5a4dd237983829e66475f0a4a26938c4b04c21baede6262215b8" +checksum = "14b9415ee827af173ebb3f15f9083df5a122eb93572ec28741fb153356ea2578" dependencies = [ "memchr", ] [[package]] name = "winreg" -version = "0.50.0" +version = "0.52.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "524e57b2c537c0f9b1e69f1965311ec12182b4122e45035b1508cd24d2adadb1" +checksum = "a277a57398d4bfa075df44f501a17cfdf8542d224f0d36095a2adc7aee4ef0a5" dependencies = [ "cfg-if", "windows-sys 0.48.0", @@ -4150,7 +4477,7 @@ checksum = "ec874e1eef0df2dcac546057fe5e29186f09c378181cd7b635b4b7bcc98e9d81" dependencies = [ "assert-json-diff", "async-trait", - "base64", + "base64 0.21.7", "deadpool", "futures", "http", @@ -4172,6 +4499,7 @@ version = "1.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "525b4ec142c6b68a2d10f01f7bbf6755599ca3f81ea53b8431b7dd348f5fdb2d" dependencies = [ + "serde", "zeroize_derive", ] @@ -4183,7 +4511,7 @@ checksum = "ce36e65b0d2999d2aafac989fb249189a141aee1f53c612c1f37d72631959f69" dependencies = [ "proc-macro2", "quote", - "syn 2.0.53", + "syn 2.0.60", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index 64920d58c..1f32654dc 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -4,6 +4,8 @@ members = ["crates/*"] # Global settings for all crates should be defined here [workspace.package] +# Update using `cargo set-version -p bitwarden ` +version = "0.5.0" authors = ["Bitwarden Inc"] edition = "2021" # Note: Changing rust-version should be considered a breaking change @@ -15,12 +17,16 @@ keywords = ["bitwarden"] # Define dependencies that are expected to be consistent across all crates [workspace.dependencies] -bitwarden = { path = "crates/bitwarden", version = "0.4.0" } -bitwarden-api-api = { path = "crates/bitwarden-api-api", version = "0.2.3" } -bitwarden-api-identity = { path = "crates/bitwarden-api-identity", version = "=0.2.3" } -bitwarden-crypto = { path = "crates/bitwarden-crypto", version = "=0.1.0" } -bitwarden-exporters = { path = "crates/bitwarden-exporters", version = "=0.1.0" } -bitwarden-generators = { path = "crates/bitwarden-generators", version = "=0.1.0" } +bitwarden = { path = "crates/bitwarden", version = "=0.5.0" } +bitwarden-api-api = { path = "crates/bitwarden-api-api", version = "=0.5.0" } +bitwarden-api-identity = { path = "crates/bitwarden-api-identity", version = "=0.5.0" } +bitwarden-cli = { path = "crates/bitwarden-cli", version = "=0.5.0" } +bitwarden-crypto = { path = "crates/bitwarden-crypto", version = "=0.5.0" } +bitwarden-exporters = { path = "crates/bitwarden-exporters", version = "=0.5.0" } +bitwarden-generators = { path = "crates/bitwarden-generators", version = "=0.5.0" } + +[workspace.lints.clippy] +unwrap_used = "deny" # Compile all dependencies with some optimizations when building this crate on debug # This slows down clean builds by about 50%, but the resulting binaries can be orders of magnitude faster diff --git a/README.md b/README.md index a1ef88b7d..6c639d052 100644 --- a/README.md +++ b/README.md @@ -140,5 +140,24 @@ VALUES ); ``` +## Developer tools + +This project recommends the use of certain developer tools, and also includes configurations for +them to make developers lives easier. The use of these tools is optional and they might require a +separate installation step. + +The list of developer tools is: + +- `Visual Studio Code`: We provide a recommended extension list which should show under the + `Extensions` tab when opening this project with the editor. We also offer a few launch settings + and tasks to build and run the SDK +- `bacon`: This is a CLI background code checker. We provide a configuration file with some of the + most common tasks to run (`check`, `clippy`, `test`, `doc` - run `bacon -l` to see them all). This + tool needs to be installed separately by running `cargo install bacon --locked`. +- `nexttest`: This is a new and faster test runner, capable of running tests in parallel and with a + much nicer output compared to `cargo test`. This tool needs to be installed separately by running + `cargo install cargo-nextest --locked`. It can be manually run using + `cargo nextest run --all-features` + [secrets-manager]: https://bitwarden.com/products/secrets-manager/ [bws-help]: https://bitwarden.com/help/secrets-manager-cli/ diff --git a/bacon.toml b/bacon.toml new file mode 100644 index 000000000..6844980d5 --- /dev/null +++ b/bacon.toml @@ -0,0 +1,78 @@ +# This is a configuration file for the bacon tool +# +# Bacon repository: https://github.com/Canop/bacon +# Complete help on configuration: https://dystroy.org/bacon/config/ +# You can also check bacon's own bacon.toml file +# as an example: https://github.com/Canop/bacon/blob/main/bacon.toml + +default_job = "check" + +[jobs.check] +command = ["cargo", "check", "--color", "always"] +need_stdout = false + +[jobs.check-all] +command = ["cargo", "check", "--all-targets", "--color", "always"] +need_stdout = false + +[jobs.clippy] +command = ["cargo", "clippy", "--all-targets", "--color", "always"] +need_stdout = false + +[jobs.test] +command = [ + "cargo", + "test", + "--all-features", + "--color", + "always", + "--", + "--color", + "always", # see https://github.com/Canop/bacon/issues/124 +] +need_stdout = true + +[jobs.doc] +command = ["cargo", "doc", "--color", "always", "--no-deps"] +need_stdout = false + +# If the doc compiles, then it opens in your browser and bacon switches +# to the previous job +[jobs.doc-open] +command = ["cargo", "doc", "--color", "always", "--no-deps", "--open"] +need_stdout = false +on_success = "back" # so that we don't open the browser at each change + +[jobs.doc-internal] +command = [ + "cargo", + "doc", + "--color", + "always", + "--no-deps", + "--all-features", + "--document-private-items", +] +need_stdout = false + +[jobs.doc-internal-open] +command = [ + "cargo", + "doc", + "--color", + "always", + "--no-deps", + "--all-features", + "--document-private-items", + "--open", +] +allow_warnings = true +need_stdout = false +on_success = "job:doc-internal" + +# You may define here keybindings that would be specific to +# a project, for example a shortcut to launch a specific job. +# Shortcuts to internal functions (scrolling, toggling, etc.) +# should go in your personal global prefs.toml file instead. +[keybindings] +# alt-m = "job:my-job" diff --git a/clippy.toml b/clippy.toml new file mode 100644 index 000000000..a29e019ac --- /dev/null +++ b/clippy.toml @@ -0,0 +1,2 @@ +allow-unwrap-in-tests=true +allow-expect-in-tests=true diff --git a/crates/bitwarden-api-api/Cargo.toml b/crates/bitwarden-api-api/Cargo.toml index 538d590ec..cfe357998 100644 --- a/crates/bitwarden-api-api/Cargo.toml +++ b/crates/bitwarden-api-api/Cargo.toml @@ -1,11 +1,11 @@ [package] name = "bitwarden-api-api" -version = "0.2.3" description = """ Api bindings for the Bitwarden API. """ categories = ["api-bindings"] +version.workspace = true authors.workspace = true edition.workspace = true rust-version.workspace = true @@ -23,7 +23,7 @@ url = ">=2.3.1, <3" uuid = { version = ">=1.3.3, <2", features = ["serde"] } [dependencies.reqwest] version = ">=0.12, <0.13" -features = ["json", "multipart"] +features = ["http2", "json", "multipart"] default-features = false [dev-dependencies] diff --git a/crates/bitwarden-api-identity/Cargo.toml b/crates/bitwarden-api-identity/Cargo.toml index 067d90648..74d96d144 100644 --- a/crates/bitwarden-api-identity/Cargo.toml +++ b/crates/bitwarden-api-identity/Cargo.toml @@ -1,11 +1,11 @@ [package] name = "bitwarden-api-identity" -version = "0.2.3" description = """ Api bindings for the Bitwarden Identity API. """ categories = ["api-bindings"] +version.workspace = true authors.workspace = true edition.workspace = true rust-version.workspace = true @@ -23,7 +23,7 @@ url = ">=2.3.1, <3" uuid = { version = ">=1.3.3, <2", features = ["serde"] } [dependencies.reqwest] version = ">=0.12, <0.13" -features = ["json", "multipart"] +features = ["http2", "json", "multipart"] default-features = false [dev-dependencies] diff --git a/crates/bitwarden-c/Cargo.toml b/crates/bitwarden-c/Cargo.toml index e3f966ee1..47d4ff5cd 100644 --- a/crates/bitwarden-c/Cargo.toml +++ b/crates/bitwarden-c/Cargo.toml @@ -21,3 +21,6 @@ bitwarden-json = { path = "../bitwarden-json", features = ["secrets"] } [dependencies] env_logger = ">=0.10.0, <0.12" + +[lints] +workspace = true diff --git a/crates/bitwarden-c/src/c.rs b/crates/bitwarden-c/src/c.rs index 934b20359..32abe3dc0 100644 --- a/crates/bitwarden-c/src/c.rs +++ b/crates/bitwarden-c/src/c.rs @@ -11,7 +11,8 @@ pub async extern "C" fn run_command( client_ptr: *const Client, ) -> *mut c_char { let client = unsafe { ffi_ref!(client_ptr) }; - let input_str = str::from_utf8(unsafe { CStr::from_ptr(c_str_ptr).to_bytes() }).unwrap(); + let input_str = str::from_utf8(unsafe { CStr::from_ptr(c_str_ptr) }.to_bytes()) + .expect("Input should be a valid string"); let result = client.run_command(input_str).await; match std::ffi::CString::new(result) { @@ -28,8 +29,8 @@ pub extern "C" fn init(c_str_ptr: *const c_char) -> *mut Client { if c_str_ptr.is_null() { box_ptr!(Client::new(None)) } else { - let input_string = str::from_utf8(unsafe { CStr::from_ptr(c_str_ptr).to_bytes() }) - .unwrap() + let input_string = str::from_utf8(unsafe { CStr::from_ptr(c_str_ptr) }.to_bytes()) + .expect("Input should be a valid string") .to_owned(); box_ptr!(Client::new(Some(input_string))) } diff --git a/crates/bitwarden-cli/Cargo.toml b/crates/bitwarden-cli/Cargo.toml index de5a4f9fc..4b21763f9 100644 --- a/crates/bitwarden-cli/Cargo.toml +++ b/crates/bitwarden-cli/Cargo.toml @@ -1,7 +1,10 @@ [package] name = "bitwarden-cli" -version = "0.1.0" +description = """ +Internal crate for the bws crate. Do not use. +""" +version.workspace = true authors.workspace = true edition.workspace = true rust-version.workspace = true @@ -11,7 +14,10 @@ license-file.workspace = true keywords.workspace = true [dependencies] -clap = { version = "4.5.1", features = ["derive"] } -color-eyre = "0.6" +clap = { version = "4.5.4", features = ["derive"] } +color-eyre = "0.6.3" inquire = "0.6.2" supports-color = "3.0.0" + +[lints] +workspace = true diff --git a/crates/bitwarden-cli/src/color.rs b/crates/bitwarden-cli/src/color.rs index 410a8b6ed..2e3a2c007 100644 --- a/crates/bitwarden-cli/src/color.rs +++ b/crates/bitwarden-cli/src/color.rs @@ -8,6 +8,9 @@ pub enum Color { } impl Color { + /** + * Evaluate if colors are supported + */ pub fn is_enabled(self) -> bool { match self { Color::No => false, @@ -17,6 +20,9 @@ impl Color { } } +/** + * Installs color_eyre, if Color is disabled we use an empty theme to disable error colors. + */ pub fn install_color_eyre(color: Color) -> color_eyre::Result<(), color_eyre::Report> { if color.is_enabled() { color_eyre::install() diff --git a/crates/bitwarden-crypto/Cargo.toml b/crates/bitwarden-crypto/Cargo.toml index ce0424c63..a23dd90b1 100644 --- a/crates/bitwarden-crypto/Cargo.toml +++ b/crates/bitwarden-crypto/Cargo.toml @@ -1,10 +1,10 @@ [package] name = "bitwarden-crypto" -version = "0.1.0" description = """ Internal crate for the bitwarden crate. Do not use. """ +version.workspace = true authors.workspace = true edition.workspace = true rust-version.workspace = true @@ -17,14 +17,15 @@ keywords.workspace = true default = [] mobile = ["dep:uniffi"] # Mobile-specific features +test = [] # Test methods [dependencies] aes = { version = ">=0.8.2, <0.9", features = ["zeroize"] } argon2 = { version = ">=0.5.0, <0.6", features = [ - "alloc", + "std", "zeroize", ], default-features = false } -base64 = ">=0.21.2, <0.22" +base64 = ">=0.21.2, <0.23" 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" @@ -41,10 +42,13 @@ 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 } +uniffi = { version = "=0.27.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" + +[lints] +workspace = true diff --git a/crates/bitwarden-crypto/src/aes.rs b/crates/bitwarden-crypto/src/aes.rs index ee76f9936..dde588563 100644 --- a/crates/bitwarden-crypto/src/aes.rs +++ b/crates/bitwarden-crypto/src/aes.rs @@ -149,7 +149,8 @@ pub fn decrypt_aes128_hmac( /// Generate a MAC using HMAC-SHA256. fn generate_mac(mac_key: &[u8], iv: &[u8], data: &[u8]) -> Result<[u8; 32]> { - let mut hmac = PbkdfSha256Hmac::new_from_slice(mac_key).expect("HMAC can take key of any size"); + let mut hmac = + PbkdfSha256Hmac::new_from_slice(mac_key).expect("hmac new_from_slice should not fail"); hmac.update(iv); hmac.update(data); let mac: [u8; PBKDF_SHA256_HMAC_OUT_SIZE] = (*hmac.finalize().into_bytes()) diff --git a/crates/bitwarden-crypto/src/enc_string/asymmetric.rs b/crates/bitwarden-crypto/src/enc_string/asymmetric.rs index f9bda838a..f9d1921ee 100644 --- a/crates/bitwarden-crypto/src/enc_string/asymmetric.rs +++ b/crates/bitwarden-crypto/src/enc_string/asymmetric.rs @@ -9,7 +9,8 @@ use super::{from_b64_vec, split_enc_string}; use crate::{ error::{CryptoError, EncStringParseError, Result}, rsa::encrypt_rsa2048_oaep_sha1, - AsymmetricCryptoKey, AsymmetricEncryptable, KeyDecryptable, + AsymmetricCryptoKey, AsymmetricEncryptable, DecryptedString, DecryptedVec, KeyDecryptable, + SensitiveVec, }; // This module is a workaround to avoid deprecated warnings that come from the ZeroizeOnDrop @@ -146,10 +147,10 @@ impl serde::Serialize for AsymmetricEncString { impl AsymmetricEncString { /// Encrypt and produce a [AsymmetricEncString::Rsa2048_OaepSha1_B64] variant. pub fn encrypt_rsa2048_oaep_sha1( - data_dec: &[u8], + data_dec: SensitiveVec, key: &dyn AsymmetricEncryptable, ) -> Result { - let enc = encrypt_rsa2048_oaep_sha1(key.to_public_key(), data_dec)?; + let enc = encrypt_rsa2048_oaep_sha1(key.to_public_key(), data_dec.expose())?; Ok(AsymmetricEncString::Rsa2048_OaepSha1_B64 { data: enc }) } @@ -166,8 +167,8 @@ impl AsymmetricEncString { } } -impl KeyDecryptable> for AsymmetricEncString { - fn decrypt_with_key(&self, key: &AsymmetricCryptoKey) -> Result> { +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), @@ -181,14 +182,15 @@ impl KeyDecryptable> for AsymmetricEncString { key.key.decrypt(Oaep::new::(), data) } } + .map(|v| DecryptedVec::new(Box::new(v))) .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) +impl KeyDecryptable for AsymmetricEncString { + fn decrypt_with_key(&self, key: &AsymmetricCryptoKey) -> Result { + let dec: DecryptedVec = self.decrypt_with_key(key)?; + dec.try_into() } } @@ -209,8 +211,11 @@ mod tests { use schemars::schema_for; use super::{AsymmetricCryptoKey, AsymmetricEncString, KeyDecryptable}; + use crate::{DecryptedString, SensitiveString}; - const RSA_PRIVATE_KEY: &str = "-----BEGIN PRIVATE KEY----- + fn rsa_private_key_string() -> SensitiveString { + SensitiveString::test( + "-----BEGIN PRIVATE KEY----- MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQCXRVrCX+2hfOQS 8HzYUS2oc/jGVTZpv+/Ryuoh9d8ihYX9dd0cYh2tl6KWdFc88lPUH11Oxqy20Rk2 e5r/RF6T9yM0Me3NPnaKt+hlhLtfoc0h86LnhD56A9FDUfuI0dVnPcrwNv0YJIo9 @@ -237,41 +242,43 @@ AoEZ18y6/KIjpSMpqC92Nnk/EBM9EYe6Cf4eA9ApAoGAeqEUg46UTlJySkBKURGp Is3v1kkf5I0X8DnOhwb+HPxNaiEdmO7ckm8+tPVgppLcG0+tMdLjigFQiDUQk2y3 WjyxP5ZvXu7U96jaJRI8PFMoE06WeVYcdIzrID2HvqH+w0UQJFrLJ/0Mn4stFAEz XKZBokBGnjFnTnKcs7nv/O8= ------END PRIVATE KEY-----"; +-----END PRIVATE KEY-----", + ) + } #[test] fn test_enc_string_rsa2048_oaep_sha256_b64() { - let private_key = AsymmetricCryptoKey::from_pem(RSA_PRIVATE_KEY).unwrap(); + let private_key = AsymmetricCryptoKey::from_pem(rsa_private_key_string()).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(); + let res: DecryptedString = 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 private_key = AsymmetricCryptoKey::from_pem(rsa_private_key_string()).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(); + let res: DecryptedString = 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 private_key = AsymmetricCryptoKey::from_pem(rsa_private_key_string()).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(); + let res: DecryptedString = enc_string.decrypt_with_key(&private_key).unwrap(); assert_eq!(res, "EncryptMe!"); } diff --git a/crates/bitwarden-crypto/src/enc_string/symmetric.rs b/crates/bitwarden-crypto/src/enc_string/symmetric.rs index d5489fc96..1a0c9f359 100644 --- a/crates/bitwarden-crypto/src/enc_string/symmetric.rs +++ b/crates/bitwarden-crypto/src/enc_string/symmetric.rs @@ -8,7 +8,7 @@ use serde::Deserialize; use super::{check_length, from_b64, from_b64_vec, split_enc_string}; use crate::{ error::{CryptoError, EncStringParseError, Result}, - KeyDecryptable, KeyEncryptable, LocateKey, SymmetricCryptoKey, + DecryptedString, DecryptedVec, KeyDecryptable, KeyEncryptable, LocateKey, SymmetricCryptoKey, }; /// # Encrypted string primitive @@ -119,15 +119,15 @@ impl EncString { match enc_type { 0 => { check_length(buf, 18)?; - let iv = buf[1..17].try_into().unwrap(); + let iv = buf[1..17].try_into().expect("Valid length"); let data = buf[17..].to_vec(); Ok(EncString::AesCbc256_B64 { iv, data }) } 1 | 2 => { check_length(buf, 50)?; - let iv = buf[1..17].try_into().unwrap(); - let mac = buf[17..49].try_into().unwrap(); + let iv = buf[1..17].try_into().expect("Valid length"); + let mac = buf[17..49].try_into().expect("Valid length"); let data = buf[49..].to_vec(); if enc_type == 1 { @@ -233,11 +233,15 @@ impl KeyEncryptable for &[u8] { } } -impl KeyDecryptable> for EncString { - fn decrypt_with_key(&self, key: &SymmetricCryptoKey) -> Result> { +impl KeyDecryptable for EncString { + fn decrypt_with_key(&self, key: &SymmetricCryptoKey) -> Result { match self { EncString::AesCbc256_B64 { iv, data } => { - let dec = crate::aes::decrypt_aes256(iv, data.clone(), &key.key)?; + let dec = DecryptedVec::new(Box::new(crate::aes::decrypt_aes256( + iv, + data.clone(), + &key.key, + )?)); Ok(dec) } EncString::AesCbc128_HmacSha256_B64 { iv, mac, data } => { @@ -247,13 +251,24 @@ impl KeyDecryptable> for EncString { // When refactoring the key handling, this should be fixed. let enc_key = key.key[0..16].into(); let mac_key = key.key[16..32].into(); - let dec = crate::aes::decrypt_aes128_hmac(iv, mac, data.clone(), mac_key, enc_key)?; + let dec = DecryptedVec::new(Box::new(crate::aes::decrypt_aes128_hmac( + iv, + mac, + data.clone(), + mac_key, + enc_key, + )?)); Ok(dec) } EncString::AesCbc256_HmacSha256_B64 { iv, mac, data } => { 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)?; + let dec = DecryptedVec::new(Box::new(crate::aes::decrypt_aes256_hmac( + iv, + mac, + data.clone(), + mac_key, + &key.key, + )?)); Ok(dec) } } @@ -266,10 +281,10 @@ impl KeyEncryptable for String { } } -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) +impl KeyDecryptable for EncString { + fn decrypt_with_key(&self, key: &SymmetricCryptoKey) -> Result { + let dec: DecryptedVec = self.decrypt_with_key(key)?; + dec.try_into() } } @@ -290,16 +305,18 @@ mod tests { use schemars::schema_for; use super::EncString; - use crate::{derive_symmetric_key, KeyDecryptable, KeyEncryptable, SymmetricCryptoKey}; + use crate::{ + derive_symmetric_key, KeyDecryptable, KeyEncryptable, SensitiveString, SymmetricCryptoKey, + }; #[test] fn test_enc_string_roundtrip() { let key = derive_symmetric_key("test"); - let test_string = "encrypted_test_string".to_string(); - let cipher = test_string.clone().encrypt_with_key(&key).unwrap(); + let test_string = "encrypted_test_string"; + let cipher = test_string.to_owned().encrypt_with_key(&key).unwrap(); - let decrypted_str: String = cipher.decrypt_with_key(&key).unwrap(); + let decrypted_str: SensitiveString = cipher.decrypt_with_key(&key).unwrap(); assert_eq!(decrypted_str, test_string); } @@ -390,27 +407,27 @@ mod tests { #[test] fn test_decrypt_cbc256() { - let key = "hvBMMb1t79YssFZkpetYsM3deyVuQv4r88Uj9gvYe08="; - let key: SymmetricCryptoKey = key.parse().unwrap(); + let key = SensitiveString::test("hvBMMb1t79YssFZkpetYsM3deyVuQv4r88Uj9gvYe08="); + let key = SymmetricCryptoKey::try_from(key).unwrap(); let enc_str = "0.NQfjHLr6za7VQVAbrpL81w==|wfrjmyJ0bfwkQlySrhw8dA=="; let enc_string: EncString = enc_str.parse().unwrap(); assert_eq!(enc_string.enc_type(), 0); - let dec_str: String = enc_string.decrypt_with_key(&key).unwrap(); + let dec_str: SensitiveString = enc_string.decrypt_with_key(&key).unwrap(); assert_eq!(dec_str, "EncryptMe!"); } #[test] fn test_decrypt_cbc128_hmac() { - let key = "Gt1aZ8kTTgkF80bLtb7LiMZBcxEA2FA5mbvV4x7K208="; - let key: SymmetricCryptoKey = key.parse().unwrap(); + let key = SensitiveString::test("Gt1aZ8kTTgkF80bLtb7LiMZBcxEA2FA5mbvV4x7K208="); + let key = SymmetricCryptoKey::try_from(key).unwrap(); let enc_str = "1.CU/oG4VZuxbHoZSDZjCLQw==|kb1HGwAk+fQ275ORfLf5Ew==|8UaEYHyqRZcG37JWhYBOBdEatEXd1u1/wN7OuImolcM="; let enc_string: EncString = enc_str.parse().unwrap(); assert_eq!(enc_string.enc_type(), 1); - let dec_str: String = enc_string.decrypt_with_key(&key).unwrap(); + let dec_str: SensitiveString = enc_string.decrypt_with_key(&key).unwrap(); assert_eq!(dec_str, "EncryptMe!"); } diff --git a/crates/bitwarden-crypto/src/encryptable.rs b/crates/bitwarden-crypto/src/encryptable.rs deleted file mode 100644 index a9882629f..000000000 --- a/crates/bitwarden-crypto/src/encryptable.rs +++ /dev/null @@ -1,95 +0,0 @@ -use std::{collections::HashMap, hash::Hash}; - -use rayon::prelude::*; -use uuid::Uuid; - -use crate::{CryptoError, KeyDecryptable, KeyEncryptable, Result, SymmetricCryptoKey}; - -pub trait KeyContainer: Send + Sync { - fn get_key(&self, org_id: &Option) -> Option<&SymmetricCryptoKey>; -} - -pub trait LocateKey { - fn locate_key<'a>( - &self, - enc: &'a dyn KeyContainer, - org_id: &Option, - ) -> Option<&'a SymmetricCryptoKey> { - enc.get_key(org_id) - } -} - -/// Deprecated: please use LocateKey and KeyDecryptable instead -pub trait Encryptable { - fn encrypt(self, enc: &dyn KeyContainer, org_id: &Option) -> Result; -} - -/// Deprecated: please use LocateKey and KeyDecryptable instead -pub trait Decryptable { - fn decrypt(&self, enc: &dyn KeyContainer, org_id: &Option) -> Result; -} - -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: &dyn KeyContainer, org_id: &Option) -> Result { - let key = self - .locate_key(enc, org_id) - .ok_or(CryptoError::MissingKey)?; - self.decrypt_with_key(key) - } -} - -impl + Send + Sync, Output: Send + Sync> Encryptable> - for Vec -{ - fn encrypt(self, enc: &dyn KeyContainer, org_id: &Option) -> Result> { - self.into_par_iter() - .map(|e| e.encrypt(enc, org_id)) - .collect() - } -} - -impl + Send + Sync, Output: Send + Sync> Decryptable> - for Vec -{ - fn decrypt(&self, enc: &dyn KeyContainer, org_id: &Option) -> Result> { - self.into_par_iter() - .map(|e| e.decrypt(enc, org_id)) - .collect() - } -} - -impl + Send + Sync, Output: Send + Sync, Id: Hash + Eq + Send + Sync> - Encryptable> for HashMap -{ - fn encrypt(self, enc: &dyn KeyContainer, org_id: &Option) -> Result> { - self.into_par_iter() - .map(|(id, e)| Ok((id, e.encrypt(enc, org_id)?))) - .collect() - } -} - -impl< - T: Decryptable + Send + Sync, - Output: Send + Sync, - Id: Hash + Eq + Copy + Send + Sync, - > Decryptable> for HashMap -{ - fn decrypt( - &self, - enc: &dyn KeyContainer, - org_id: &Option, - ) -> Result> { - self.into_par_iter() - .map(|(id, e)| Ok((*id, e.decrypt(enc, org_id)?))) - .collect() - } -} diff --git a/crates/bitwarden-crypto/src/error.rs b/crates/bitwarden-crypto/src/error.rs index cf9a9b048..7cfb354d7 100644 --- a/crates/bitwarden-crypto/src/error.rs +++ b/crates/bitwarden-crypto/src/error.rs @@ -28,6 +28,9 @@ pub enum CryptoError { #[error("Fingerprint error, {0}")] FingerprintError(#[from] FingerprintError), + #[error("Argon2 error, {0}")] + ArgonError(#[from] argon2::Error), + #[error("Number is zero")] ZeroNumber, } diff --git a/crates/bitwarden-crypto/src/fingerprint.rs b/crates/bitwarden-crypto/src/fingerprint.rs index d3e69d577..2e7200344 100644 --- a/crates/bitwarden-crypto/src/fingerprint.rs +++ b/crates/bitwarden-crypto/src/fingerprint.rs @@ -6,7 +6,6 @@ use num_bigint::BigUint; use num_traits::cast::ToPrimitive; -use sha2::Digest; use thiserror::Error; use crate::{error::Result, wordlist::EFF_LONG_WORD_LIST, CryptoError}; @@ -17,10 +16,6 @@ use crate::{error::Result, wordlist::EFF_LONG_WORD_LIST, CryptoError}; /// - `fingerprint_material`: user's id. /// - `public_key`: user's public key. 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(|_| CryptoError::InvalidKeyLen)?; @@ -51,7 +46,10 @@ fn hash_word(hash: [u8; 32]) -> Result { let remainder = hash_number.clone() % EFF_LONG_WORD_LIST.len(); hash_number /= EFF_LONG_WORD_LIST.len(); - phrase.push(EFF_LONG_WORD_LIST[remainder.to_usize().unwrap()].to_string()); + let index = remainder + .to_usize() + .expect("Remainder is less than EFF_LONG_WORD_LIST.len()"); + phrase.push(EFF_LONG_WORD_LIST[index].to_string()); } Ok(phrase.join("-")) diff --git a/crates/bitwarden-crypto/src/keys/asymmetric_crypto_key.rs b/crates/bitwarden-crypto/src/keys/asymmetric_crypto_key.rs index be523bbc6..13230659b 100644 --- a/crates/bitwarden-crypto/src/keys/asymmetric_crypto_key.rs +++ b/crates/bitwarden-crypto/src/keys/asymmetric_crypto_key.rs @@ -3,7 +3,10 @@ use std::pin::Pin; use rsa::{pkcs8::DecodePublicKey, RsaPrivateKey, RsaPublicKey}; use super::key_encryptable::CryptoKey; -use crate::error::{CryptoError, Result}; +use crate::{ + error::{CryptoError, Result}, + SensitiveString, SensitiveVec, +}; /// Trait to allow both [`AsymmetricCryptoKey`] and [`AsymmetricPublicCryptoKey`] to be used to /// encrypt [AsymmetricEncString](crate::AsymmetricEncString). @@ -65,38 +68,47 @@ impl AsymmetricCryptoKey { } } - pub fn from_pem(pem: &str) -> Result { + pub fn from_pem(pem: SensitiveString) -> Result { use rsa::pkcs8::DecodePrivateKey; Ok(Self { - key: Box::pin(RsaPrivateKey::from_pkcs8_pem(pem).map_err(|_| CryptoError::InvalidKey)?), + key: Box::pin( + RsaPrivateKey::from_pkcs8_pem(pem.expose()).map_err(|_| CryptoError::InvalidKey)?, + ), }) } - pub fn from_der(der: &[u8]) -> Result { + pub fn from_der(der: SensitiveVec) -> Result { use rsa::pkcs8::DecodePrivateKey; Ok(Self { - key: Box::pin(RsaPrivateKey::from_pkcs8_der(der).map_err(|_| CryptoError::InvalidKey)?), + key: Box::pin( + RsaPrivateKey::from_pkcs8_der(der.expose()).map_err(|_| CryptoError::InvalidKey)?, + ), }) } - pub fn to_der(&self) -> Result> { + pub fn to_der(&self) -> Result { use rsa::pkcs8::EncodePrivateKey; - Ok(self + + // SecretDocument implements ZeroizeOnDrop + let key = self .key .to_pkcs8_der() - .map_err(|_| CryptoError::InvalidKey)? - .as_bytes() - .to_owned()) + .map_err(|_| CryptoError::InvalidKey)?; + + Ok(SensitiveVec::new(Box::new(key.as_bytes().to_owned()))) } pub fn to_public_der(&self) -> Result> { use rsa::pkcs8::EncodePublicKey; - Ok(self + + // SecretDocument implements ZeroizeOnDrop + let key = self .to_public_key() .to_public_key_der() - .map_err(|_| CryptoError::InvalidKey)? - .as_bytes() - .to_owned()) + .map_err(|_| CryptoError::InvalidKey)?; + + // Public keys are considered not sensitive + Ok(key.as_bytes().to_owned()) } } @@ -120,12 +132,14 @@ mod tests { use base64::{engine::general_purpose::STANDARD, Engine}; use crate::{ - AsymmetricCryptoKey, AsymmetricEncString, AsymmetricPublicCryptoKey, KeyDecryptable, + AsymmetricCryptoKey, AsymmetricEncString, AsymmetricPublicCryptoKey, DecryptedString, + KeyDecryptable, SensitiveString, }; #[test] fn test_asymmetric_crypto_key() { - let pem_key_str = "-----BEGIN PRIVATE KEY----- + let pem_key_str = SensitiveString::test( + "-----BEGIN PRIVATE KEY----- MIIEwAIBADANBgkqhkiG9w0BAQEFAASCBKowggSmAgEAAoIBAQDiTQVuzhdygFz5 qv14i+XFDGTnDravzUQT1hPKPGUZOUSZ1gwdNgkWqOIaOnR65BHEnL0sp4bnuiYc afeK2JAW5Sc8Z7IxBNSuAwhQmuKx3RochMIiuCkI2/p+JvUQoJu6FBNm8OoJ4Cwm @@ -152,52 +166,53 @@ Zy2o6PPxhfkagaDjqEeN9Lrs5LD4nEvDkr5CG1vOjmMCgYEAvIBFKRm31NyF8jLi CVuPwC5PzrW5iThDmsWTaXFpB3esUsbICO2pEz872oeQS+Em4GO5vXUlpbbFPzup PFhA8iMJ8TAvemhvc7oM0OZqpU6p3K4seHf6BkwLxumoA3vDJfovu9RuXVcJVOnf DnqOsltgPomWZ7xVfMkm9niL2OA= ------END PRIVATE KEY-----"; +-----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(); + let der_key_vec = SensitiveString::test("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=").decode_base64(STANDARD).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(); + let der_key = AsymmetricCryptoKey::from_der(der_key_vec.clone()).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); + assert_eq!(der_key_vec, der_key.to_der().unwrap()); + assert_eq!(der_key_vec, pem_key.to_der().unwrap()); } #[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 private_key = SensitiveString::test(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=", + )) + .decode_base64(STANDARD) + .unwrap(); let public_key = STANDARD .decode(concat!( @@ -207,18 +222,18 @@ DnqOsltgPomWZ7xVfMkm9niL2OA= "LkdZfs9X5qKHiTxc+VklzTm3PSap4eORTQ/lP1GB10y0qJk5+44GRcSQSr3ku6ui", "2re8AJ2GQhdnZz5oWaCb/kij5bQPBwBrIEBlgRdaeasVdR6wFJPJAQZxtqWo9MPK", "eVDOkaQ3Qrryh+49S4rln3592/WeHYM5hO47DJr86ELcqcyCmksYas7xTqHfVfHS", - "XQIDAQAB", + "XQIDAQAB" )) .unwrap(); - let private_key = AsymmetricCryptoKey::from_der(&private_key).unwrap(); + let private_key = AsymmetricCryptoKey::from_der(private_key).unwrap(); let public_key = AsymmetricPublicCryptoKey::from_der(&public_key).unwrap(); - let plaintext = "Hello, world!"; + let plaintext = SensitiveString::test("Hello, world!"); let encrypted = - AsymmetricEncString::encrypt_rsa2048_oaep_sha1(plaintext.as_bytes(), &public_key) + AsymmetricEncString::encrypt_rsa2048_oaep_sha1(plaintext.clone().into(), &public_key) .unwrap(); - let decrypted: String = encrypted.decrypt_with_key(&private_key).unwrap(); + let decrypted: DecryptedString = 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 index 876ae7b75..1bc2ed02c 100644 --- a/crates/bitwarden-crypto/src/keys/device_key.rs +++ b/crates/bitwarden-crypto/src/keys/device_key.rs @@ -1,8 +1,6 @@ -use std::str::FromStr; - use crate::{ - error::Result, AsymmetricCryptoKey, AsymmetricEncString, CryptoError, EncString, - KeyDecryptable, KeyEncryptable, SymmetricCryptoKey, + error::Result, AsymmetricCryptoKey, AsymmetricEncString, CryptoError, DecryptedVec, EncString, + KeyDecryptable, KeyEncryptable, SensitiveString, SymmetricCryptoKey, }; /// Device Key @@ -16,7 +14,7 @@ pub struct DeviceKey(SymmetricCryptoKey); #[cfg_attr(feature = "mobile", derive(uniffi::Record))] pub struct TrustDeviceResponse { /// Base64 encoded device key - pub device_key: String, + pub device_key: SensitiveString, /// UserKey encrypted with DevicePublicKey pub protected_user_key: AsymmetricEncString, /// DevicePrivateKey encrypted with [DeviceKey] @@ -40,7 +38,7 @@ impl DeviceKey { let data = user_key.to_vec(); let protected_user_key = - AsymmetricEncString::encrypt_rsa2048_oaep_sha1(&data, &device_private_key)?; + AsymmetricEncString::encrypt_rsa2048_oaep_sha1(data, &device_private_key)?; let protected_device_public_key = device_private_key .to_public_der()? @@ -48,6 +46,7 @@ impl DeviceKey { let protected_device_private_key = device_private_key .to_der()? + .expose() .encrypt_with_key(&device_key.0)?; Ok(TrustDeviceResponse { @@ -64,26 +63,26 @@ impl DeviceKey { 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 device_private_key: DecryptedVec = + protected_device_private_key.decrypt_with_key(&self.0)?; + let device_private_key = AsymmetricCryptoKey::from_der(device_private_key)?; - let mut dec: Vec = protected_user_key.decrypt_with_key(&device_private_key)?; - let user_key: SymmetricCryptoKey = dec.as_mut_slice().try_into()?; + let dec: DecryptedVec = protected_user_key.decrypt_with_key(&device_private_key)?; + let user_key = SymmetricCryptoKey::try_from(dec)?; Ok(user_key) } - fn to_base64(&self) -> String { + fn to_base64(&self) -> SensitiveString { self.0.to_base64() } } -impl FromStr for DeviceKey { - type Err = CryptoError; +impl TryFrom for DeviceKey { + type Error = CryptoError; - fn from_str(s: &str) -> Result { - let key = s.parse::()?; - Ok(DeviceKey(key)) + fn try_from(value: SensitiveString) -> Result { + SymmetricCryptoKey::try_from(value).map(DeviceKey) } } @@ -98,10 +97,8 @@ mod tests { let result = DeviceKey::trust_device(&key).unwrap(); - let decrypted = result - .device_key - .parse::() - .unwrap() + let device_key = DeviceKey::try_from(result.device_key).unwrap(); + let decrypted = device_key .decrypt_user_key( result.protected_device_private_key, result.protected_user_key, diff --git a/crates/bitwarden-crypto/src/keys/key_encryptable.rs b/crates/bitwarden-crypto/src/keys/key_encryptable.rs index 750647ee8..d8f11a011 100644 --- a/crates/bitwarden-crypto/src/keys/key_encryptable.rs +++ b/crates/bitwarden-crypto/src/keys/key_encryptable.rs @@ -1,8 +1,23 @@ use std::{collections::HashMap, hash::Hash}; use rayon::prelude::*; +use uuid::Uuid; -use crate::error::Result; +use crate::{error::Result, SymmetricCryptoKey}; + +pub trait KeyContainer: Send + Sync { + fn get_key(&self, org_id: &Option) -> Option<&SymmetricCryptoKey>; +} + +pub trait LocateKey { + fn locate_key<'a>( + &self, + enc: &'a dyn KeyContainer, + org_id: &Option, + ) -> Option<&'a SymmetricCryptoKey> { + enc.get_key(org_id) + } +} pub trait CryptoKey {} diff --git a/crates/bitwarden-crypto/src/keys/master_key.rs b/crates/bitwarden-crypto/src/keys/master_key.rs index 8e6d2575b..0015d624d 100644 --- a/crates/bitwarden-crypto/src/keys/master_key.rs +++ b/crates/bitwarden-crypto/src/keys/master_key.rs @@ -1,12 +1,19 @@ use std::num::NonZeroU32; -use base64::{engine::general_purpose::STANDARD, Engine}; +use base64::engine::general_purpose::STANDARD; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; use super::utils::{derive_kdf_key, stretch_kdf_key}; -use crate::{util, CryptoError, EncString, KeyDecryptable, Result, SymmetricCryptoKey, UserKey}; +use crate::{ + util, CryptoError, DecryptedVec, EncString, KeyDecryptable, Result, SensitiveString, + SensitiveVec, SymmetricCryptoKey, UserKey, +}; +/// Key Derivation Function for Bitwarden Account +/// +/// In Bitwarden accounts can use multiple KDFs to derive their master key from their password. This +/// Enum represents all the possible KDFs. #[derive(Serialize, Deserialize, Debug, JsonSchema, Clone)] #[serde(rename_all = "camelCase", deny_unknown_fields)] #[cfg_attr(feature = "mobile", derive(uniffi::Enum))] @@ -21,6 +28,32 @@ pub enum Kdf { }, } +impl Default for Kdf { + /// Default KDF for new accounts. + fn default() -> Self { + Kdf::PBKDF2 { + iterations: default_pbkdf2_iterations(), + } + } +} + +/// Default PBKDF2 iterations +pub fn default_pbkdf2_iterations() -> NonZeroU32 { + NonZeroU32::new(600_000).expect("Non-zero number") +} +/// Default Argon2 iterations +pub fn default_argon2_iterations() -> NonZeroU32 { + NonZeroU32::new(3).expect("Non-zero number") +} +/// Default Argon2 memory +pub fn default_argon2_memory() -> NonZeroU32 { + NonZeroU32::new(64).expect("Non-zero number") +} +/// Default Argon2 parallelism +pub fn default_argon2_parallelism() -> NonZeroU32 { + NonZeroU32::new(4).expect("Non-zero number") +} + #[derive(Copy, Clone, JsonSchema)] #[cfg_attr(feature = "mobile", derive(uniffi::Enum))] pub enum HashPurpose { @@ -31,6 +64,7 @@ pub enum HashPurpose { /// Master Key. /// /// Derived from the users master password, used to protect the [UserKey]. +#[derive(Debug)] pub struct MasterKey(SymmetricCryptoKey); impl MasterKey { @@ -39,15 +73,18 @@ impl MasterKey { } /// Derives a users master key from their password, email and KDF. - pub fn derive(password: &[u8], email: &[u8], kdf: &Kdf) -> Result { + pub fn derive(password: &SensitiveVec, email: &[u8], kdf: &Kdf) -> Result { derive_kdf_key(password, email, kdf).map(Self) } /// 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 fn derive_master_key_hash( + &self, + password: &SensitiveVec, + purpose: HashPurpose, + ) -> Result { + let hash = util::pbkdf2(&self.0.key, password.expose(), purpose as u32); + Ok(hash.encode_base64(STANDARD)) } /// Generate a new random user key and encrypt it with the master key. @@ -59,15 +96,15 @@ impl MasterKey { pub fn decrypt_user_key(&self, user_key: EncString) -> Result { let stretched_key = stretch_kdf_key(&self.0)?; - let mut dec: Vec = user_key.decrypt_with_key(&stretched_key)?; - SymmetricCryptoKey::try_from(dec.as_mut_slice()) + let dec: DecryptedVec = user_key.decrypt_with_key(&stretched_key)?; + SymmetricCryptoKey::try_from(dec) } pub fn encrypt_user_key(&self, user_key: &SymmetricCryptoKey) -> Result { let stretched_key = stretch_kdf_key(&self.0)?; EncString::encrypt_aes256_hmac( - user_key.to_vec().as_slice(), + user_key.to_vec().expose(), stretched_key .mac_key .as_ref() @@ -94,13 +131,15 @@ mod tests { use rand::SeedableRng; use super::{make_user_key, HashPurpose, Kdf, MasterKey}; - use crate::{keys::symmetric_crypto_key::derive_symmetric_key, SymmetricCryptoKey}; + use crate::{ + keys::symmetric_crypto_key::derive_symmetric_key, SensitiveVec, SymmetricCryptoKey, + }; #[test] fn test_master_key_derive_pbkdf2() { let master_key = MasterKey::derive( - &b"67t9b5g67$%Dh89n"[..], - "test_key".as_bytes(), + &SensitiveVec::test(b"67t9b5g67$%Dh89n"), + b"test_key", &Kdf::PBKDF2 { iterations: NonZeroU32::new(10000).unwrap(), }, @@ -120,8 +159,8 @@ mod tests { #[test] fn test_master_key_derive_argon2() { let master_key = MasterKey::derive( - &b"67t9b5g67$%Dh89n"[..], - "test_key".as_bytes(), + &SensitiveVec::test(b"67t9b5g67$%Dh89n"), + b"test_key", &Kdf::Argon2id { iterations: NonZeroU32::new(4).unwrap(), memory: NonZeroU32::new(32).unwrap(), @@ -142,39 +181,39 @@ mod tests { #[test] fn test_password_hash_pbkdf2() { - let password = "asdfasdf".as_bytes(); - let salt = "test_salt".as_bytes(); + let password = SensitiveVec::test(b"asdfasdf"); + let salt = b"test_salt"; let kdf = Kdf::PBKDF2 { iterations: NonZeroU32::new(100_000).unwrap(), }; - let master_key = MasterKey::derive(password, salt, &kdf).unwrap(); + let master_key = MasterKey::derive(&password, salt, &kdf).unwrap(); assert_eq!( - "ZF6HjxUTSyBHsC+HXSOhZoXN+UuMnygV5YkWXCY4VmM=", master_key - .derive_master_key_hash(password, HashPurpose::ServerAuthorization) + .derive_master_key_hash(&password, HashPurpose::ServerAuthorization) .unwrap(), + "ZF6HjxUTSyBHsC+HXSOhZoXN+UuMnygV5YkWXCY4VmM=", ); } #[test] fn test_password_hash_argon2id() { - let password = "asdfasdf".as_bytes(); - let salt = "test_salt".as_bytes(); + let password = SensitiveVec::test(b"asdfasdf"); + let salt = b"test_salt"; let kdf = Kdf::Argon2id { iterations: NonZeroU32::new(4).unwrap(), memory: NonZeroU32::new(32).unwrap(), parallelism: NonZeroU32::new(2).unwrap(), }; - let master_key = MasterKey::derive(password, salt, &kdf).unwrap(); + let master_key = MasterKey::derive(&password, salt, &kdf).unwrap(); assert_eq!( - "PR6UjYmjmppTYcdyTiNbAhPJuQQOmynKbdEl1oyi/iQ=", master_key - .derive_master_key_hash(password, HashPurpose::ServerAuthorization) + .derive_master_key_hash(&password, HashPurpose::ServerAuthorization) .unwrap(), + "PR6UjYmjmppTYcdyTiNbAhPJuQQOmynKbdEl1oyi/iQ=", ); } diff --git a/crates/bitwarden-crypto/src/keys/mod.rs b/crates/bitwarden-crypto/src/keys/mod.rs index 5bfd32b8b..ac1732966 100644 --- a/crates/bitwarden-crypto/src/keys/mod.rs +++ b/crates/bitwarden-crypto/src/keys/mod.rs @@ -1,7 +1,10 @@ mod key_encryptable; -pub use key_encryptable::{KeyDecryptable, KeyEncryptable}; +pub use key_encryptable::{CryptoKey, KeyContainer, KeyDecryptable, KeyEncryptable, LocateKey}; mod master_key; -pub use master_key::{HashPurpose, Kdf, MasterKey}; +pub use master_key::{ + default_argon2_iterations, default_argon2_memory, default_argon2_parallelism, + default_pbkdf2_iterations, HashPurpose, Kdf, MasterKey, +}; mod shareable_key; pub use shareable_key::derive_shareable_key; mod symmetric_crypto_key; diff --git a/crates/bitwarden-crypto/src/keys/pin_key.rs b/crates/bitwarden-crypto/src/keys/pin_key.rs index 475b7ffd9..261743a58 100644 --- a/crates/bitwarden-crypto/src/keys/pin_key.rs +++ b/crates/bitwarden-crypto/src/keys/pin_key.rs @@ -3,7 +3,7 @@ use crate::{ key_encryptable::CryptoKey, utils::{derive_kdf_key, stretch_kdf_key}, }, - EncString, Kdf, KeyEncryptable, Result, SymmetricCryptoKey, + EncString, Kdf, KeyEncryptable, Result, SensitiveVec, SymmetricCryptoKey, }; /// Pin Key. @@ -17,7 +17,7 @@ impl PinKey { } /// Derives a users pin key from their password, email and KDF. - pub fn derive(password: &[u8], salt: &[u8], kdf: &Kdf) -> Result { + pub fn derive(password: &SensitiveVec, salt: &[u8], kdf: &Kdf) -> Result { derive_kdf_key(password, salt, kdf).map(Self) } } diff --git a/crates/bitwarden-crypto/src/keys/shareable_key.rs b/crates/bitwarden-crypto/src/keys/shareable_key.rs index fbb2d2e44..495aa8a9b 100644 --- a/crates/bitwarden-crypto/src/keys/shareable_key.rs +++ b/crates/bitwarden-crypto/src/keys/shareable_key.rs @@ -2,44 +2,59 @@ use std::pin::Pin; use aes::cipher::typenum::U64; use generic_array::GenericArray; -use hmac::{Hmac, Mac}; +use hmac::Mac; +use zeroize::Zeroize; -use crate::{keys::SymmetricCryptoKey, util::hkdf_expand}; +use crate::{ + keys::SymmetricCryptoKey, + util::{hkdf_expand, PbkdfSha256Hmac}, + Sensitive, +}; /// 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 fn derive_shareable_key( - secret: [u8; 16], + secret: Sensitive<[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 - let res = Hmac::::new_from_slice(format!("bitwarden-{}", name).as_bytes()) - .unwrap() - .chain_update(secret) + let mut res = PbkdfSha256Hmac::new_from_slice(format!("bitwarden-{}", name).as_bytes()) + .expect("hmac new_from_slice should not fail") + .chain_update(secret.expose()) .finalize() .into_bytes(); - let mut key: Pin>> = hkdf_expand(&res, info).unwrap(); + let mut key: Pin>> = + hkdf_expand(&res, info).expect("Input is a valid size"); + + // Zeroize the temporary buffer + res.zeroize(); - SymmetricCryptoKey::try_from(key.as_mut_slice()).unwrap() + SymmetricCryptoKey::try_from(key.as_mut_slice()).expect("Key is a valid size") } #[cfg(test)] mod tests { use super::derive_shareable_key; + use crate::Sensitive; #[test] fn test_derive_shareable_key() { - let key = derive_shareable_key(*b"&/$%F1a895g67HlX", "test_key", None); + let key = derive_shareable_key( + Sensitive::new(Box::new(*b"&/$%F1a895g67HlX")), + "test_key", + None, + ); assert_eq!(key.to_base64(), "4PV6+PcmF2w7YHRatvyMcVQtI7zvCyssv/wFWmzjiH6Iv9altjmDkuBD1aagLVaLezbthbSe+ktR+U6qswxNnQ=="); - let key = derive_shareable_key(*b"67t9b5g67$%Dh89n", "test_key", Some("test")); + let key = derive_shareable_key( + Sensitive::new(Box::new(*b"67t9b5g67$%Dh89n")), + "test_key", + Some("test"), + ); assert_eq!(key.to_base64(), "F9jVQmrACGx9VUPjuzfMYDjr726JtL300Y3Yg+VYUnVQtQ1s8oImJ5xtp1KALC9h2nav04++1LDW4iFD+infng=="); } } diff --git a/crates/bitwarden-crypto/src/keys/symmetric_crypto_key.rs b/crates/bitwarden-crypto/src/keys/symmetric_crypto_key.rs index 0c165bb40..c5cbbd1d0 100644 --- a/crates/bitwarden-crypto/src/keys/symmetric_crypto_key.rs +++ b/crates/bitwarden-crypto/src/keys/symmetric_crypto_key.rs @@ -1,13 +1,13 @@ -use std::{pin::Pin, str::FromStr}; +use std::pin::Pin; use aes::cipher::typenum::U32; -use base64::{engine::general_purpose::STANDARD, Engine}; +use base64::engine::general_purpose::STANDARD; use generic_array::GenericArray; use rand::Rng; -use zeroize::{Zeroize, Zeroizing}; +use zeroize::Zeroize; use super::key_encryptable::CryptoKey; -use crate::CryptoError; +use crate::{CryptoError, Sensitive, SensitiveString, SensitiveVec}; /// A symmetric encryption key. Used to encrypt and decrypt [`EncString`](crate::EncString) pub struct SymmetricCryptoKey { @@ -59,31 +59,44 @@ impl SymmetricCryptoKey { 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_base64(&self) -> SensitiveString { + self.to_vec().encode_base64(STANDARD) } - pub fn to_vec(&self) -> Zeroizing> { - let mut buf = Vec::with_capacity(self.total_len()); + pub fn to_vec(&self) -> SensitiveVec { + let mut buf = SensitiveVec::new(Box::new(Vec::with_capacity(self.total_len()))); - buf.extend_from_slice(&self.key); + buf.expose_mut().extend_from_slice(&self.key); if let Some(mac) = &self.mac_key { - buf.extend_from_slice(mac); + buf.expose_mut().extend_from_slice(mac); } - Zeroizing::new(buf) + buf + } +} + +impl TryFrom for SymmetricCryptoKey { + type Error = CryptoError; + + fn try_from(value: SensitiveString) -> Result { + SymmetricCryptoKey::try_from(value.decode_base64(STANDARD)?) } } -impl FromStr for SymmetricCryptoKey { - type Err = CryptoError; +impl TryFrom> for SymmetricCryptoKey { + type Error = CryptoError; - fn from_str(s: &str) -> Result { - let mut bytes = STANDARD.decode(s).map_err(|_| CryptoError::InvalidKey)?; - SymmetricCryptoKey::try_from(bytes.as_mut_slice()) + fn try_from(mut value: Sensitive<[u8; N]>) -> Result { + let val = value.expose_mut(); + SymmetricCryptoKey::try_from(val.as_mut_slice()) + } +} + +impl TryFrom for SymmetricCryptoKey { + type Error = CryptoError; + + fn try_from(mut value: SensitiveVec) -> Result { + let val = value.expose_mut(); + SymmetricCryptoKey::try_from(val.as_mut_slice()) } } @@ -132,25 +145,24 @@ impl std::fmt::Debug for SymmetricCryptoKey { pub fn derive_symmetric_key(name: &str) -> SymmetricCryptoKey { use crate::{derive_shareable_key, generate_random_bytes}; - let secret: [u8; 16] = generate_random_bytes(); + let secret: Sensitive<[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}; + use crate::SensitiveString; #[test] fn test_symmetric_crypto_key() { let key = derive_symmetric_key("test"); - let key2 = SymmetricCryptoKey::from_str(&key.to_base64()).unwrap(); + let key2 = SymmetricCryptoKey::try_from(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(); + let key = SensitiveString::test("UY4B5N4DA4UisCNClgZtRr6VLy9ZF5BXXC7cDZRqourKi4ghEMgISbCsubvgCkHf5DZctQjVot11/vVvN9NNHQ=="); + let key2 = SymmetricCryptoKey::try_from(key.clone()).unwrap(); assert_eq!(key, key2.to_base64()); } } diff --git a/crates/bitwarden-crypto/src/keys/utils.rs b/crates/bitwarden-crypto/src/keys/utils.rs index d83e212d0..cfe528f99 100644 --- a/crates/bitwarden-crypto/src/keys/utils.rs +++ b/crates/bitwarden-crypto/src/keys/utils.rs @@ -3,12 +3,17 @@ use std::pin::Pin; use generic_array::{typenum::U32, GenericArray}; use sha2::Digest; -use crate::{util::hkdf_expand, Kdf, Result, SymmetricCryptoKey}; +use crate::{util::hkdf_expand, Kdf, Result, Sensitive, SensitiveVec, SymmetricCryptoKey}; /// Derive a generic key from a secret and salt using the provided KDF. -pub(super) fn derive_kdf_key(secret: &[u8], salt: &[u8], kdf: &Kdf) -> Result { - let mut hash = match kdf { - Kdf::PBKDF2 { iterations } => crate::util::pbkdf2(secret, salt, iterations.get()), +#[inline(always)] +pub(super) fn derive_kdf_key( + secret: &SensitiveVec, + salt: &[u8], + kdf: &Kdf, +) -> Result { + let hash = match kdf { + Kdf::PBKDF2 { iterations } => crate::util::pbkdf2(secret.expose(), salt, iterations.get()), Kdf::Argon2id { iterations, @@ -25,20 +30,27 @@ pub(super) fn derive_kdf_key(secret: &[u8], salt: &[u8], kdf: &Kdf) -> Result Result { diff --git a/crates/bitwarden-crypto/src/lib.rs b/crates/bitwarden-crypto/src/lib.rs index 3c1aced24..9ba3a7eee 100644 --- a/crates/bitwarden-crypto/src/lib.rs +++ b/crates/bitwarden-crypto/src/lib.rs @@ -1,15 +1,49 @@ //! # 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`]. +//! This crate contains the cryptographic primitives used throughout the SDK. The general +//! aspiration is for this crate to handle all the difficult cryptographic operations and expose +//! higher level concepts to the rest of the SDK. //! -//! ## Conventions: +//!
+//! Generally you should not find yourself needing to edit this crate! Everything written +//! here requires additional care and attention to ensure that the cryptographic primitives are +//! secure.
+//! +//! ## Example: +//! +//! ```rust +//! use bitwarden_crypto::{ +//! CryptoError, DecryptedString, KeyDecryptable, KeyEncryptable, SymmetricCryptoKey, +//! }; +//! +//! async fn example() -> Result<(), CryptoError> { +//! let key = SymmetricCryptoKey::generate(rand::thread_rng()); +//! +//! let data = "Hello, World!".to_owned(); +//! let encrypted = data.clone().encrypt_with_key(&key)?; +//! let decrypted: DecryptedString = encrypted.decrypt_with_key(&key)?; +//! +//! assert_eq!(decrypted, data); +//! Ok(()) +//! } +//! ``` +//! +//! ## Development considerations +//! +//! This crate is expected to provide long term support for cryptographic operations. To that end, +//! the following considerations should be taken into account when making changes to this crate: +//! +//! - Limit public interfaces to the bare minimum. +//! - Breaking changes should be rare and well communicated. +//! - Serializable representation of keys and encrypted data must be supported indefinitely as we +//! have no way to update all data. +//! +//! ### 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` +//! ### 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 @@ -24,8 +58,6 @@ 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; @@ -40,6 +72,8 @@ pub use util::generate_random_bytes; mod wordlist; pub use util::pbkdf2; pub use wordlist::EFF_LONG_WORD_LIST; +mod sensitive; +pub use sensitive::*; #[cfg(feature = "mobile")] uniffi::setup_scaffolding!(); diff --git a/crates/bitwarden-crypto/src/rsa.rs b/crates/bitwarden-crypto/src/rsa.rs index 98f1282cc..6e3658c04 100644 --- a/crates/bitwarden-crypto/src/rsa.rs +++ b/crates/bitwarden-crypto/src/rsa.rs @@ -21,6 +21,7 @@ pub struct RsaKeyPair { pub private: EncString, } +/// Generate a new RSA key pair of 2048 bits pub(crate) fn make_key_pair(key: &SymmetricCryptoKey) -> Result { let mut rng = rand::thread_rng(); let bits = 2048; @@ -48,6 +49,7 @@ pub(crate) fn make_key_pair(key: &SymmetricCryptoKey) -> Result { }) } +/// Encrypt data using RSA-OAEP-SHA1 with a 2048 bit key pub(super) fn encrypt_rsa2048_oaep_sha1(public_key: &RsaPublicKey, data: &[u8]) -> Result> { let mut rng = rand::thread_rng(); diff --git a/crates/bitwarden-crypto/src/sensitive/decrypted.rs b/crates/bitwarden-crypto/src/sensitive/decrypted.rs new file mode 100644 index 000000000..02096d081 --- /dev/null +++ b/crates/bitwarden-crypto/src/sensitive/decrypted.rs @@ -0,0 +1,16 @@ +use zeroize::Zeroize; + +use crate::{CryptoError, CryptoKey, KeyEncryptable, Sensitive}; + +/// Type alias for a [`Sensitive`] value to denote decrypted data. +pub type Decrypted = Sensitive; +pub type DecryptedVec = Decrypted>; +pub type DecryptedString = Decrypted; + +impl + Zeroize + Clone, Key: CryptoKey, Output> + KeyEncryptable for Decrypted +{ + fn encrypt_with_key(self, key: &Key) -> Result { + self.value.clone().encrypt_with_key(key) + } +} diff --git a/crates/bitwarden-crypto/src/sensitive/mod.rs b/crates/bitwarden-crypto/src/sensitive/mod.rs new file mode 100644 index 000000000..0da2329b9 --- /dev/null +++ b/crates/bitwarden-crypto/src/sensitive/mod.rs @@ -0,0 +1,5 @@ +#[allow(clippy::module_inception)] +mod sensitive; +pub use sensitive::{Sensitive, SensitiveString, SensitiveVec}; +mod decrypted; +pub use decrypted::{Decrypted, DecryptedString, DecryptedVec}; diff --git a/crates/bitwarden-crypto/src/sensitive/sensitive.rs b/crates/bitwarden-crypto/src/sensitive/sensitive.rs new file mode 100644 index 000000000..4ae779f05 --- /dev/null +++ b/crates/bitwarden-crypto/src/sensitive/sensitive.rs @@ -0,0 +1,307 @@ +use std::{ + borrow::Cow, + fmt::{self, Formatter}, +}; + +use generic_array::{ArrayLength, GenericArray}; +use schemars::JsonSchema; +use serde::{Deserialize, Deserializer, Serialize, Serializer}; +use zeroize::{Zeroize, ZeroizeOnDrop}; + +use crate::CryptoError; + +/// Wrapper for sensitive values which makes a best effort to enforce zeroization of the inner value +/// on drop. The inner value exposes a [`Sensitive::expose`] method which returns a reference to the +/// inner value. Care must be taken to avoid accidentally exposing the inner value through copying +/// or cloning. +/// +/// Internally [`Sensitive`] contains a [`Box`] which ensures the value is placed on the heap. It +/// implements the [`Drop`] trait which calls `zeroize` on the inner value. +#[derive(Eq, Clone, Zeroize, ZeroizeOnDrop)] +pub struct Sensitive { + pub(super) value: Box, +} + +/// Important: This type does not protect against reallocations made by the Vec. +/// This means that if you insert any elements past the capacity, the data will be copied to a +/// new allocation and the old allocation will not be zeroized. +/// To avoid this, use Vec::with_capacity to preallocate the capacity you need. +pub type SensitiveVec = Sensitive>; + +/// Important: This type does not protect against reallocations made by the String. +/// This means that if you insert any characters past the capacity, the data will be copied to a +/// new allocation and the old allocation will not be zeroized. +/// To avoid this, use String::with_capacity to preallocate the capacity you need. +pub type SensitiveString = Sensitive; + +impl Sensitive { + /// Create a new [`Sensitive`] value. In an attempt to avoid accidentally placing this on the + /// stack it only accepts a [`Box`] value. The rust compiler should be able to optimize away the + /// initial stack allocation presuming the value is not used before being boxed. + #[inline(always)] + pub fn new(value: Box) -> Self { + Self { value } + } + + /// Expose the inner value. By exposing the inner value, you take responsibility for ensuring + /// that any copy of the value is zeroized. + #[inline(always)] + pub fn expose(&self) -> &V { + &self.value + } + + /// Expose the inner value mutable. By exposing the inner value, you take responsibility for + /// ensuring that any copy of the value is zeroized. + #[inline(always)] + pub fn expose_mut(&mut self) -> &mut V { + &mut self.value + } +} + +/// Helper to convert a `Sensitive<[u8, N]>` to a `SensitiveVec`. +impl From> for SensitiveVec { + fn from(sensitive: Sensitive<[u8; N]>) -> Self { + SensitiveVec::new(Box::new(sensitive.value.to_vec())) + } +} + +/// Helper to convert a `&SensitiveVec` to a `Sensitive<[u8, N]>`. +impl TryFrom<&SensitiveVec> for Sensitive<[u8; N]> { + type Error = CryptoError; + + fn try_from(v: &SensitiveVec) -> Result { + Ok(Sensitive::new(Box::new( + TryInto::<[u8; N]>::try_into(v.expose().as_slice()) + .map_err(|_| CryptoError::InvalidKey)?, + ))) + } +} + +/// Helper to convert a `Sensitive>` to a `Sensitive`, care is taken to ensure any +/// intermediate copies are zeroed to avoid leaking sensitive data. +impl TryFrom for SensitiveString { + type Error = CryptoError; + + fn try_from(mut v: SensitiveVec) -> Result { + let value = std::mem::take(&mut v.value); + + let rtn = String::from_utf8(*value).map_err(|_| CryptoError::InvalidUtf8String); + rtn.map(|v| Sensitive::new(Box::new(v))) + } +} + +impl From for SensitiveVec { + fn from(mut s: SensitiveString) -> Self { + let value = std::mem::take(&mut s.value); + Sensitive::new(Box::new(value.into_bytes())) + } +} + +impl> From>> for SensitiveVec { + fn from(val: Sensitive>) -> Self { + SensitiveVec::new(Box::new(val.value.to_vec())) + } +} + +impl SensitiveString { + pub fn decode_base64(self, engine: T) -> Result { + // Prevent accidental copies by allocating the full size + let len = base64::decoded_len_estimate(self.value.len()); + let mut value = SensitiveVec::new(Box::new(Vec::with_capacity(len))); + + engine + .decode_vec(self.value.as_ref(), &mut value.value) + .map_err(|_| CryptoError::InvalidKey)?; + + Ok(value) + } + + #[inline(always)] + pub fn len(&self) -> usize { + self.value.len() + } + + #[inline(always)] + pub fn is_empty(&self) -> bool { + self.value.is_empty() + } + + // The predicate is specifically a fn() and not a closure to forbid capturing values + // from the environment, which would make it easier to accidentally leak some data. + // For example, the following won't compile with fn() but would work with impl Fn(): + // ``` + // let mut chars = Mutex::new(Vec::new()); + // self.any_chars(|c| {chars.lock().unwrap().push(c); true}); + // ``` + // Note that this is not a perfect solution, as it is still possible to leak the characters by + // using a global variable or a static variable. Also `char` implements Copy so it's hard to + // ensure the compiler is not making a copy of the character. + #[inline(always)] + pub fn any_chars(&self, predicate: fn(char) -> bool) -> bool { + self.value.chars().any(predicate) + } +} + +impl> Sensitive { + pub fn encode_base64(self, engine: E) -> SensitiveString { + use base64::engine::Config; + + let inner: &[u8] = self.value.as_ref().as_ref(); + + // Prevent accidental copies by allocating the full size + let padding = engine.config().encode_padding(); + let len = base64::encoded_len(inner.len(), padding).expect("Valid length"); + + let mut value = SensitiveVec::new(Box::new(vec![0u8; len])); + engine + .encode_slice(inner, &mut value.value[..len]) + .expect("Valid base64 string length"); + + value.try_into().expect("Valid base64 string") + } +} + +impl Default for Sensitive { + fn default() -> Self { + Self::new(Box::default()) + } +} + +impl fmt::Debug for Sensitive { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + f.debug_struct("Sensitive") + .field("type", &std::any::type_name::()) + .field("value", &"********") + .finish() + } +} + +impl> PartialEq> for Sensitive { + fn eq(&self, other: &Self) -> bool { + self.value.eq(&other.value) + } +} +impl> PartialEq for Sensitive { + fn eq(&self, other: &V) -> bool { + self.value.as_ref().eq(other) + } +} +impl PartialEq<&str> for SensitiveString { + fn eq(&self, other: &&str) -> bool { + self.value.as_ref().eq(other) + } +} +impl PartialEq<&[u8]> for SensitiveVec { + fn eq(&self, other: &&[u8]) -> bool { + self.value.as_ref().eq(other) + } +} + +/// Unfortunately once we serialize a `SensitiveString` we can't control the future memory. +impl Serialize for Sensitive { + fn serialize(&self, serializer: S) -> Result { + self.value.serialize(serializer) + } +} + +impl<'de, V: Zeroize + Deserialize<'de>> Deserialize<'de> for Sensitive { + fn deserialize>(deserializer: D) -> Result { + Ok(Self::new(Box::new(V::deserialize(deserializer)?))) + } +} + +/// Transparently expose the inner value for serialization +impl JsonSchema for Sensitive { + fn schema_name() -> String { + V::schema_name() + } + + fn schema_id() -> Cow<'static, str> { + V::schema_id() + } + + fn json_schema(gen: &mut schemars::gen::SchemaGenerator) -> schemars::schema::Schema { + V::json_schema(gen) + } +} + +// We use a lot of `&str` and `&[u8]` in our tests, so we expose this helper +// to make it easier. +// IMPORTANT: This should not be used outside of test code +#[cfg(any(test, feature = "test"))] +impl Sensitive { + pub fn test(value: &'static T) -> Self + where + &'static T: Into, + { + Self::new(Box::new(value.into())) + } +} + +#[cfg(test)] +mod tests { + use schemars::schema_for; + + use super::*; + + #[test] + fn test_debug() { + let string = SensitiveString::test("test"); + assert_eq!( + format!("{:?}", string), + "Sensitive { type: \"alloc::string::String\", value: \"********\" }" + ); + + let vector = Sensitive::new(Box::new(vec![1, 2, 3])); + assert_eq!( + format!("{:?}", vector), + "Sensitive { type: \"alloc::vec::Vec\", value: \"********\" }" + ); + } + + #[test] + fn test_schemars() { + #[derive(JsonSchema)] + struct TestStruct { + #[allow(dead_code)] + s: SensitiveString, + #[allow(dead_code)] + v: SensitiveVec, + } + + let schema = schema_for!(TestStruct); + let json = serde_json::to_string_pretty(&schema).unwrap(); + let expected = r##"{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "TestStruct", + "type": "object", + "required": ["s", "v"], + "properties": { + "s": { + "$ref": "#/definitions/String" + }, + "v": { + "$ref": "#/definitions/Array_of_uint8" + } + }, + "definitions": { + "Array_of_uint8": { + "type": "array", + "items": { + "type": "integer", + "format": "uint8", + "minimum": 0.0 + } + }, + "String": { + "type": "string" + } + } + }"##; + + assert_eq!( + json.parse::().unwrap(), + expected.parse::().unwrap() + ); + } +} diff --git a/crates/bitwarden-crypto/src/uniffi_support.rs b/crates/bitwarden-crypto/src/uniffi_support.rs index 7f1249da0..f7269f96c 100644 --- a/crates/bitwarden-crypto/src/uniffi_support.rs +++ b/crates/bitwarden-crypto/src/uniffi_support.rs @@ -1,6 +1,8 @@ use std::{num::NonZeroU32, str::FromStr}; -use crate::{AsymmetricEncString, CryptoError, EncString, UniffiCustomTypeConverter}; +use crate::{ + AsymmetricEncString, CryptoError, EncString, SensitiveString, UniffiCustomTypeConverter, +}; uniffi::custom_type!(NonZeroU32, u32); @@ -43,3 +45,17 @@ impl UniffiCustomTypeConverter for AsymmetricEncString { obj.to_string() } } + +uniffi::custom_type!(SensitiveString, String); + +impl UniffiCustomTypeConverter for SensitiveString { + type Builtin = String; + + fn into_custom(val: Self::Builtin) -> uniffi::Result { + Ok(SensitiveString::new(Box::new(val))) + } + + fn from_custom(obj: Self) -> Self::Builtin { + obj.expose().to_owned() + } +} diff --git a/crates/bitwarden-crypto/src/util.rs b/crates/bitwarden-crypto/src/util.rs index ba60db366..166294612 100644 --- a/crates/bitwarden-crypto/src/util.rs +++ b/crates/bitwarden-crypto/src/util.rs @@ -7,8 +7,9 @@ use rand::{ distributions::{Distribution, Standard}, Rng, }; +use zeroize::Zeroize; -use crate::{CryptoError, Result}; +use crate::{CryptoError, Result, Sensitive}; pub(crate) type PbkdfSha256Hmac = hmac::Hmac; pub(crate) const PBKDF_SHA256_HMAC_OUT_SIZE: usize = @@ -30,16 +31,26 @@ pub(crate) fn hkdf_expand>( } /// Generate random bytes that are cryptographically secure -pub fn generate_random_bytes() -> T +pub fn generate_random_bytes() -> Sensitive where Standard: Distribution, + T: Zeroize, { - rand::thread_rng().gen() + Sensitive::new(Box::new(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") +#[inline(always)] +pub fn pbkdf2( + password: &[u8], + salt: &[u8], + rounds: u32, +) -> Sensitive<[u8; PBKDF_SHA256_HMAC_OUT_SIZE]> { + let mut hash = Sensitive::new(Box::new([0u8; PBKDF_SHA256_HMAC_OUT_SIZE])); + + pbkdf2::pbkdf2::(password, salt, rounds, hash.expose_mut()) + .expect("hash is a valid fixed size"); + + hash } #[cfg(test)] diff --git a/crates/bitwarden-crypto/src/wordlist.rs b/crates/bitwarden-crypto/src/wordlist.rs index 4cc30ff74..f1e7a59c0 100644 --- a/crates/bitwarden-crypto/src/wordlist.rs +++ b/crates/bitwarden-crypto/src/wordlist.rs @@ -1,4 +1,4 @@ -// EFF's Long Wordlist from https://www.eff.org/dice +/// EFF's Long Wordlist from pub const EFF_LONG_WORD_LIST: &[&str] = &[ "abacus", "abdomen", diff --git a/crates/bitwarden-exporters/Cargo.toml b/crates/bitwarden-exporters/Cargo.toml index 0607369d0..9ed860af6 100644 --- a/crates/bitwarden-exporters/Cargo.toml +++ b/crates/bitwarden-exporters/Cargo.toml @@ -1,11 +1,11 @@ [package] name = "bitwarden-exporters" -version = "0.1.0" description = """ Internal crate for the bitwarden crate. Do not use. """ exclude = ["/resources"] +version.workspace = true authors.workspace = true edition.workspace = true rust-version.workspace = true @@ -15,7 +15,7 @@ license-file.workspace = true keywords.workspace = true [dependencies] -base64 = ">=0.21.2, <0.22" +base64 = ">=0.21.2, <0.23" bitwarden-crypto = { workspace = true } chrono = { version = ">=0.4.26, <0.5", features = [ "clock", @@ -23,7 +23,14 @@ chrono = { version = ">=0.4.26, <0.5", features = [ "std", ], default-features = false } csv = "1.3.0" +itertools = "0.12.1" serde = { version = ">=1.0, <2.0", features = ["derive"] } serde_json = ">=1.0.96, <2.0" thiserror = ">=1.0.40, <2.0" uuid = { version = ">=1.3.3, <2.0", features = ["serde", "v4"] } + +[dev-dependencies] +bitwarden-crypto = { workspace = true, features = ["test"] } + +[lints] +workspace = true diff --git a/crates/bitwarden-exporters/src/csv.rs b/crates/bitwarden-exporters/src/csv.rs index 644eeb030..d4bbe5174 100644 --- a/crates/bitwarden-exporters/src/csv.rs +++ b/crates/bitwarden-exporters/src/csv.rs @@ -1,9 +1,9 @@ use std::collections::HashMap; +use bitwarden_crypto::{DecryptedString, Sensitive}; use csv::Writer; use serde::Serializer; use thiserror::Error; -use uuid::Uuid; use crate::{Cipher, CipherType, Field, Folder}; @@ -14,7 +14,7 @@ pub enum CsvError { } pub(crate) fn export_csv(folders: Vec, ciphers: Vec) -> Result { - let folders: HashMap = folders.into_iter().map(|f| (f.id, f.name)).collect(); + let folders: HashMap<_, _> = folders.into_iter().map(|f| (f.id, f.name)).collect(); let rows = ciphers .into_iter() @@ -48,38 +48,41 @@ pub(crate) fn export_csv(folders: Vec, ciphers: Vec) -> Result /// /// Be careful when changing this struct to maintain compatibility with old exports. #[derive(serde::Serialize)] struct CsvRow { - folder: Option, + folder: Option, #[serde(serialize_with = "bool_serialize")] favorite: bool, r#type: String, - name: String, - notes: Option, + name: DecryptedString, + notes: Option, #[serde(serialize_with = "fields_serialize")] fields: Vec, reprompt: u8, #[serde(serialize_with = "vec_serialize")] - login_uri: Vec, - login_username: Option, - login_password: Option, - login_totp: Option, + login_uri: Vec, + login_username: Option, + login_password: Option, + login_totp: Option, } -fn vec_serialize(x: &[String], s: S) -> Result +fn vec_serialize(x: &[DecryptedString], s: S) -> Result where S: Serializer, { - s.serialize_str(x.join(",").as_str()) + let iter = itertools::Itertools::intersperse(x.iter().map(|s| s.expose().as_str()), ","); + let result: Sensitive = Sensitive::new(Box::new(iter.collect())); + + s.serialize_str(result.expose()) } fn bool_serialize(x: &bool, s: S) -> Result @@ -98,8 +101,14 @@ where .map(|f| { format!( "{}: {}", - f.name.to_owned().unwrap_or_default(), - f.value.to_owned().unwrap_or_default() + f.name + .as_ref() + .map(|n| n.expose().as_str()) + .unwrap_or_default(), + f.value + .as_ref() + .map(|n| n.expose().as_str()) + .unwrap_or_default(), ) }) .collect::>() @@ -118,24 +127,24 @@ mod tests { let folders = vec![ Folder { id: "d55d65d7-c161-40a4-94ca-b0d20184d91a".parse().unwrap(), - name: "Test Folder A".to_string(), + name: DecryptedString::test("Test Folder A"), }, Folder { id: "583e7665-0126-4d37-9139-b0d20184dd86".parse().unwrap(), - name: "Test Folder B".to_string(), + name: DecryptedString::test("Test Folder B"), }, ]; let ciphers = vec![ Cipher { id: "d55d65d7-c161-40a4-94ca-b0d20184d91a".parse().unwrap(), folder_id: None, - name: "test@bitwarden.com".to_string(), + name: DecryptedString::test("test@bitwarden.com"), notes: None, r#type: CipherType::Login(Box::new(Login { - username: Some("test@bitwarden.com".to_string()), - password: Some("Abc123".to_string()), + username: Some(DecryptedString::test("test@bitwarden.com")), + password: Some(DecryptedString::test("Abc123")), login_uris: vec![LoginUri { - uri: Some("https://google.com".to_string()), + uri: Some(DecryptedString::test("https://google.com")), r#match: None, }], totp: None, @@ -150,29 +159,29 @@ mod tests { Cipher { id: "7dd81bd0-cc72-4f42-96e7-b0fc014e71a3".parse().unwrap(), folder_id: Some("583e7665-0126-4d37-9139-b0d20184dd86".parse().unwrap()), - name: "Steam Account".to_string(), + name: DecryptedString::test("Steam Account"), notes: None, r#type: CipherType::Login(Box::new(Login { - username: Some("steam".to_string()), - password: Some("3Pvb8u7EfbV*nJ".to_string()), + username: Some(DecryptedString::test("steam")), + password: Some(DecryptedString::test("3Pvb8u7EfbV*nJ")), login_uris: vec![LoginUri { - uri: Some("https://steampowered.com".to_string()), + uri: Some(DecryptedString::test("https://steampowered.com")), r#match: None, }], - totp: Some("steam://ABCD123".to_string()), + totp: Some(DecryptedString::test("steam://ABCD123")), })), favorite: true, reprompt: 0, fields: vec![ Field { - name: Some("Test".to_string()), - value: Some("v".to_string()), + name: Some(DecryptedString::test("Test")), + value: Some(DecryptedString::test("v")), r#type: 0, linked_id: None, }, Field { - name: Some("Hidden".to_string()), - value: Some("asdfer".to_string()), + name: Some(DecryptedString::test("Hidden")), + value: Some(DecryptedString::test("asdfer")), r#type: 1, linked_id: None, }, @@ -200,7 +209,7 @@ mod tests { let ciphers = vec![Cipher { id: "d55d65d7-c161-40a4-94ca-b0d20184d91a".parse().unwrap(), folder_id: None, - name: "My Card".to_string(), + name: DecryptedString::test("My Card"), notes: None, r#type: CipherType::Card(Box::new(Card { cardholder_name: None, @@ -229,7 +238,7 @@ mod tests { let ciphers = vec![Cipher { id: "d55d65d7-c161-40a4-94ca-b0d20184d91a".parse().unwrap(), folder_id: None, - name: "My Identity".to_string(), + name: DecryptedString::test("My Identity"), notes: None, r#type: CipherType::Identity(Box::new(Identity { title: None, diff --git a/crates/bitwarden-exporters/src/encrypted_json.rs b/crates/bitwarden-exporters/src/encrypted_json.rs index 1bbfd2660..31b31a827 100644 --- a/crates/bitwarden-exporters/src/encrypted_json.rs +++ b/crates/bitwarden-exporters/src/encrypted_json.rs @@ -1,5 +1,7 @@ -use base64::{engine::general_purpose::STANDARD, Engine}; -use bitwarden_crypto::{generate_random_bytes, Kdf, KeyEncryptable, PinKey}; +use base64::engine::general_purpose::STANDARD; +use bitwarden_crypto::{ + generate_random_bytes, Kdf, KeyEncryptable, PinKey, Sensitive, SensitiveString, SensitiveVec, +}; use serde::Serialize; use thiserror::Error; use uuid::Uuid; @@ -24,7 +26,7 @@ pub enum EncryptedJsonError { pub(crate) fn export_encrypted_json( folders: Vec, ciphers: Vec, - password: String, + password: SensitiveString, kdf: Kdf, ) -> Result { let decrypted_export = export_json(folders, ciphers)?; @@ -43,16 +45,16 @@ pub(crate) fn export_encrypted_json( ), }; - let salt: [u8; 16] = generate_random_bytes(); - let salt = STANDARD.encode(salt); - let key = PinKey::derive(password.as_bytes(), salt.as_bytes(), &kdf)?; + let salt: Sensitive<[u8; 16]> = generate_random_bytes(); + let salt = SensitiveVec::from(salt).encode_base64(STANDARD); + let key = PinKey::derive(&password.into(), salt.expose().as_bytes(), &kdf)?; let enc_key_validation = Uuid::new_v4().to_string(); let encrypted_export = EncryptedJsonExport { encrypted: true, password_protected: true, - salt, + salt: salt.expose().to_string(), kdf_type, kdf_iterations, kdf_memory, @@ -83,6 +85,8 @@ pub(crate) struct EncryptedJsonExport { mod tests { use std::num::NonZeroU32; + use bitwarden_crypto::DecryptedString; + use super::*; use crate::{ Card, Cipher, CipherType, Field, Identity, Login, LoginUri, SecureNote, SecureNoteType, @@ -93,24 +97,24 @@ mod tests { let _export = export_encrypted_json( vec![Folder { id: "942e2984-1b9a-453b-b039-b107012713b9".parse().unwrap(), - name: "Important".to_string(), + name: DecryptedString::test("Important"), }], vec![ Cipher { id: "25c8c414-b446-48e9-a1bd-b10700bbd740".parse().unwrap(), folder_id: Some("942e2984-1b9a-453b-b039-b107012713b9".parse().unwrap()), - name: "Bitwarden".to_string(), - notes: Some("My note".to_string()), + name: DecryptedString::test("Bitwarden"), + notes: Some(DecryptedString::test("My note")), r#type: CipherType::Login(Box::new(Login { - username: Some("test@bitwarden.com".to_string()), - password: Some("asdfasdfasdf".to_string()), + username: Some(DecryptedString::test("test@bitwarden.com")), + password: Some(DecryptedString::test("asdfasdfasdf")), login_uris: vec![LoginUri { - uri: Some("https://vault.bitwarden.com".to_string()), + uri: Some(DecryptedString::test("https://vault.bitwarden.com")), r#match: None, }], - totp: Some("ABC".to_string()), + totp: Some(DecryptedString::test("ABC")), })), favorite: true, @@ -118,31 +122,31 @@ mod tests { fields: vec![ Field { - name: Some("Text".to_string()), - value: Some("A".to_string()), + name: Some(DecryptedString::test("Text")), + value: Some(DecryptedString::test("A")), r#type: 0, linked_id: None, }, Field { - name: Some("Hidden".to_string()), - value: Some("B".to_string()), + name: Some(DecryptedString::test("Hidden")), + value: Some(DecryptedString::test("B")), r#type: 1, linked_id: None, }, Field { - name: Some("Boolean (true)".to_string()), - value: Some("true".to_string()), + name: Some(DecryptedString::test("Boolean (true)")), + value: Some(DecryptedString::test("true")), r#type: 2, linked_id: None, }, Field { - name: Some("Boolean (false)".to_string()), - value: Some("false".to_string()), + name: Some(DecryptedString::test("Boolean (false)")), + value: Some(DecryptedString::test("false")), r#type: 2, linked_id: None, }, Field { - name: Some("Linked".to_string()), + name: Some(DecryptedString::test("Linked")), value: None, r#type: 3, linked_id: Some(101), @@ -157,8 +161,8 @@ mod tests { id: "23f0f877-42b1-4820-a850-b10700bc41eb".parse().unwrap(), folder_id: None, - name: "My secure note".to_string(), - notes: Some("Very secure!".to_string()), + name: DecryptedString::test("My secure note"), + notes: Some(DecryptedString::test("Very secure!")), r#type: CipherType::SecureNote(Box::new(SecureNote { r#type: SecureNoteType::Generic, @@ -177,16 +181,16 @@ mod tests { id: "3ed8de45-48ee-4e26-a2dc-b10701276c53".parse().unwrap(), folder_id: None, - name: "My card".to_string(), + name: DecryptedString::test("My card"), notes: None, r#type: CipherType::Card(Box::new(Card { - cardholder_name: Some("John Doe".to_string()), - exp_month: Some("1".to_string()), - exp_year: Some("2032".to_string()), - code: Some("123".to_string()), - brand: Some("Visa".to_string()), - number: Some("4111111111111111".to_string()), + cardholder_name: Some(DecryptedString::test("John Doe")), + exp_month: Some(DecryptedString::test("1")), + exp_year: Some(DecryptedString::test("2032")), + code: Some(DecryptedString::test("123")), + brand: Some(DecryptedString::test("Visa")), + number: Some(DecryptedString::test("4111111111111111")), })), favorite: false, @@ -202,14 +206,14 @@ mod tests { id: "41cc3bc1-c3d9-4637-876c-b10701273712".parse().unwrap(), folder_id: Some("942e2984-1b9a-453b-b039-b107012713b9".parse().unwrap()), - name: "My identity".to_string(), + name: DecryptedString::test("My identity"), notes: None, r#type: CipherType::Identity(Box::new(Identity { - title: Some("Mr".to_string()), - first_name: Some("John".to_string()), + title: Some(DecryptedString::test("Mr")), + first_name: Some(DecryptedString::test("John")), middle_name: None, - last_name: Some("Doe".to_string()), + last_name: Some(DecryptedString::test("Doe")), address1: None, address2: None, address3: None, @@ -217,11 +221,11 @@ mod tests { state: None, postal_code: None, country: None, - company: Some("Bitwarden".to_string()), + company: Some(DecryptedString::test("Bitwarden")), email: None, phone: None, ssn: None, - username: Some("JDoe".to_string()), + username: Some(DecryptedString::test("JDoe")), passport_number: None, license_number: None, })), @@ -236,7 +240,7 @@ mod tests { deleted_date: None, }, ], - "password".to_string(), + SensitiveString::test("password"), Kdf::PBKDF2 { iterations: NonZeroU32::new(600_000).unwrap(), }, diff --git a/crates/bitwarden-exporters/src/json.rs b/crates/bitwarden-exporters/src/json.rs index 3f6c72c1f..f2c889f9f 100644 --- a/crates/bitwarden-exporters/src/json.rs +++ b/crates/bitwarden-exporters/src/json.rs @@ -1,3 +1,4 @@ +use bitwarden_crypto::DecryptedString; use chrono::{DateTime, Utc}; use thiserror::Error; use uuid::Uuid; @@ -36,7 +37,7 @@ struct JsonExport { #[serde(rename_all = "camelCase")] struct JsonFolder { id: Uuid, - name: String, + name: DecryptedString, } impl From for JsonFolder { @@ -57,8 +58,8 @@ struct JsonCipher { organization_id: Option, collection_ids: Option>, - name: String, - notes: Option, + name: DecryptedString, + notes: Option, r#type: u8, #[serde(skip_serializing_if = "Option::is_none")] @@ -75,7 +76,7 @@ struct JsonCipher { #[serde(skip_serializing_if = "Vec::is_empty")] fields: Vec, - password_history: Option>, + password_history: Option>, revision_date: DateTime, creation_date: DateTime, @@ -85,11 +86,11 @@ struct JsonCipher { #[derive(serde::Serialize)] #[serde(rename_all = "camelCase")] struct JsonLogin { - username: Option, - password: Option, + username: Option, + password: Option, uris: Vec, - totp: Option, - fido2_credentials: Vec, + totp: Option, + fido2_credentials: Vec, } impl From for JsonLogin { @@ -107,7 +108,7 @@ impl From for JsonLogin { #[derive(serde::Serialize)] #[serde(rename_all = "camelCase")] struct JsonLoginUri { - uri: Option, + uri: Option, r#match: Option, } @@ -137,12 +138,12 @@ impl From for JsonSecureNote { #[derive(serde::Serialize)] #[serde(rename_all = "camelCase")] struct JsonCard { - cardholder_name: Option, - exp_month: Option, - exp_year: Option, - code: Option, - brand: Option, - number: Option, + cardholder_name: Option, + exp_month: Option, + exp_year: Option, + code: Option, + brand: Option, + number: Option, } impl From for JsonCard { @@ -161,24 +162,24 @@ impl From for JsonCard { #[derive(serde::Serialize)] #[serde(rename_all = "camelCase")] struct JsonIdentity { - title: Option, - first_name: Option, - middle_name: Option, - last_name: Option, - address1: Option, - address2: Option, - address3: Option, - city: Option, - state: Option, - postal_code: Option, - country: Option, - company: Option, - email: Option, - phone: Option, - ssn: Option, - username: Option, - passport_number: Option, - license_number: Option, + title: Option, + first_name: Option, + middle_name: Option, + last_name: Option, + address1: Option, + address2: Option, + address3: Option, + city: Option, + state: Option, + postal_code: Option, + country: Option, + company: Option, + email: Option, + phone: Option, + ssn: Option, + username: Option, + passport_number: Option, + license_number: Option, } impl From for JsonIdentity { @@ -209,8 +210,8 @@ impl From for JsonIdentity { #[derive(serde::Serialize)] #[serde(rename_all = "camelCase")] struct JsonField { - name: Option, - value: Option, + name: Option, + value: Option, r#type: u8, linked_id: Option, } @@ -278,17 +279,17 @@ mod tests { id: "25c8c414-b446-48e9-a1bd-b10700bbd740".parse().unwrap(), folder_id: Some("942e2984-1b9a-453b-b039-b107012713b9".parse().unwrap()), - name: "Bitwarden".to_string(), - notes: Some("My note".to_string()), + name: DecryptedString::test("Bitwarden"), + notes: Some(DecryptedString::test("My note")), r#type: CipherType::Login(Box::new(Login { - username: Some("test@bitwarden.com".to_string()), - password: Some("asdfasdfasdf".to_string()), + username: Some(DecryptedString::test("test@bitwarden.com")), + password: Some(DecryptedString::test("asdfasdfasdf")), login_uris: vec![LoginUri { - uri: Some("https://vault.bitwarden.com".to_string()), + uri: Some(DecryptedString::test("https://vault.bitwarden.com")), r#match: None, }], - totp: Some("ABC".to_string()), + totp: Some(DecryptedString::test("ABC")), })), favorite: true, @@ -296,31 +297,31 @@ mod tests { fields: vec![ Field { - name: Some("Text".to_string()), - value: Some("A".to_string()), + name: Some(DecryptedString::test("Text")), + value: Some(DecryptedString::test("A")), r#type: 0, linked_id: None, }, Field { - name: Some("Hidden".to_string()), - value: Some("B".to_string()), + name: Some(DecryptedString::test("Hidden")), + value: Some(DecryptedString::test("B")), r#type: 1, linked_id: None, }, Field { - name: Some("Boolean (true)".to_string()), - value: Some("true".to_string()), + name: Some(DecryptedString::test("Boolean (true)")), + value: Some(DecryptedString::test("true")), r#type: 2, linked_id: None, }, Field { - name: Some("Boolean (false)".to_string()), - value: Some("false".to_string()), + name: Some(DecryptedString::test("Boolean (false)")), + value: Some(DecryptedString::test("false")), r#type: 2, linked_id: None, }, Field { - name: Some("Linked".to_string()), + name: Some(DecryptedString::test("Linked")), value: None, r#type: 3, linked_id: Some(101), @@ -406,8 +407,8 @@ mod tests { id: "23f0f877-42b1-4820-a850-b10700bc41eb".parse().unwrap(), folder_id: None, - name: "My secure note".to_string(), - notes: Some("Very secure!".to_string()), + name: DecryptedString::test("My secure note"), + notes: Some(DecryptedString::test("Very secure!")), r#type: CipherType::SecureNote(Box::new(SecureNote { r#type: SecureNoteType::Generic, @@ -456,16 +457,16 @@ mod tests { id: "3ed8de45-48ee-4e26-a2dc-b10701276c53".parse().unwrap(), folder_id: None, - name: "My card".to_string(), + name: DecryptedString::test("My card"), notes: None, r#type: CipherType::Card(Box::new(Card { - cardholder_name: Some("John Doe".to_string()), - exp_month: Some("1".to_string()), - exp_year: Some("2032".to_string()), - code: Some("123".to_string()), - brand: Some("Visa".to_string()), - number: Some("4111111111111111".to_string()), + cardholder_name: Some(DecryptedString::test("John Doe")), + exp_month: Some(DecryptedString::test("1")), + exp_year: Some(DecryptedString::test("2032")), + code: Some(DecryptedString::test("123")), + brand: Some(DecryptedString::test("Visa")), + number: Some(DecryptedString::test("4111111111111111")), })), favorite: false, @@ -516,14 +517,14 @@ mod tests { id: "41cc3bc1-c3d9-4637-876c-b10701273712".parse().unwrap(), folder_id: Some("942e2984-1b9a-453b-b039-b107012713b9".parse().unwrap()), - name: "My identity".to_string(), + name: DecryptedString::test("My identity"), notes: None, r#type: CipherType::Identity(Box::new(Identity { - title: Some("Mr".to_string()), - first_name: Some("John".to_string()), + title: Some(DecryptedString::test("Mr")), + first_name: Some(DecryptedString::test("John")), middle_name: None, - last_name: Some("Doe".to_string()), + last_name: Some(DecryptedString::test("Doe")), address1: None, address2: None, address3: None, @@ -531,11 +532,11 @@ mod tests { state: None, postal_code: None, country: None, - company: Some("Bitwarden".to_string()), + company: Some(DecryptedString::test("Bitwarden")), email: None, phone: None, ssn: None, - username: Some("JDoe".to_string()), + username: Some(DecryptedString::test("JDoe")), passport_number: None, license_number: None, })), @@ -608,24 +609,24 @@ mod tests { let export = export_json( vec![Folder { id: "942e2984-1b9a-453b-b039-b107012713b9".parse().unwrap(), - name: "Important".to_string(), + name: DecryptedString::test("Important"), }], vec![ Cipher { id: "25c8c414-b446-48e9-a1bd-b10700bbd740".parse().unwrap(), folder_id: Some("942e2984-1b9a-453b-b039-b107012713b9".parse().unwrap()), - name: "Bitwarden".to_string(), - notes: Some("My note".to_string()), + name: DecryptedString::test("Bitwarden"), + notes: Some(DecryptedString::test("My note")), r#type: CipherType::Login(Box::new(Login { - username: Some("test@bitwarden.com".to_string()), - password: Some("asdfasdfasdf".to_string()), + username: Some(DecryptedString::test("test@bitwarden.com")), + password: Some(DecryptedString::test("asdfasdfasdf")), login_uris: vec![LoginUri { - uri: Some("https://vault.bitwarden.com".to_string()), + uri: Some(DecryptedString::test("https://vault.bitwarden.com")), r#match: None, }], - totp: Some("ABC".to_string()), + totp: Some(DecryptedString::test("ABC")), })), favorite: true, @@ -633,31 +634,31 @@ mod tests { fields: vec![ Field { - name: Some("Text".to_string()), - value: Some("A".to_string()), + name: Some(DecryptedString::test("Text")), + value: Some(DecryptedString::test("A")), r#type: 0, linked_id: None, }, Field { - name: Some("Hidden".to_string()), - value: Some("B".to_string()), + name: Some(DecryptedString::test("Hidden")), + value: Some(DecryptedString::test("B")), r#type: 1, linked_id: None, }, Field { - name: Some("Boolean (true)".to_string()), - value: Some("true".to_string()), + name: Some(DecryptedString::test("Boolean (true)")), + value: Some(DecryptedString::test("true")), r#type: 2, linked_id: None, }, Field { - name: Some("Boolean (false)".to_string()), - value: Some("false".to_string()), + name: Some(DecryptedString::test("Boolean (false)")), + value: Some(DecryptedString::test("false")), r#type: 2, linked_id: None, }, Field { - name: Some("Linked".to_string()), + name: Some(DecryptedString::test("Linked")), value: None, r#type: 3, linked_id: Some(101), @@ -672,8 +673,8 @@ mod tests { id: "23f0f877-42b1-4820-a850-b10700bc41eb".parse().unwrap(), folder_id: None, - name: "My secure note".to_string(), - notes: Some("Very secure!".to_string()), + name: DecryptedString::test("My secure note"), + notes: Some(DecryptedString::test("Very secure!")), r#type: CipherType::SecureNote(Box::new(SecureNote { r#type: SecureNoteType::Generic, @@ -692,16 +693,16 @@ mod tests { id: "3ed8de45-48ee-4e26-a2dc-b10701276c53".parse().unwrap(), folder_id: None, - name: "My card".to_string(), + name: DecryptedString::test("My card"), notes: None, r#type: CipherType::Card(Box::new(Card { - cardholder_name: Some("John Doe".to_string()), - exp_month: Some("1".to_string()), - exp_year: Some("2032".to_string()), - code: Some("123".to_string()), - brand: Some("Visa".to_string()), - number: Some("4111111111111111".to_string()), + cardholder_name: Some(DecryptedString::test("John Doe")), + exp_month: Some(DecryptedString::test("1")), + exp_year: Some(DecryptedString::test("2032")), + code: Some(DecryptedString::test("123")), + brand: Some(DecryptedString::test("Visa")), + number: Some(DecryptedString::test("4111111111111111")), })), favorite: false, @@ -717,14 +718,14 @@ mod tests { id: "41cc3bc1-c3d9-4637-876c-b10701273712".parse().unwrap(), folder_id: Some("942e2984-1b9a-453b-b039-b107012713b9".parse().unwrap()), - name: "My identity".to_string(), + name: DecryptedString::test("My identity"), notes: None, r#type: CipherType::Identity(Box::new(Identity { - title: Some("Mr".to_string()), - first_name: Some("John".to_string()), + title: Some(DecryptedString::test("Mr")), + first_name: Some(DecryptedString::test("John")), middle_name: None, - last_name: Some("Doe".to_string()), + last_name: Some(DecryptedString::test("Doe")), address1: None, address2: None, address3: None, @@ -732,11 +733,11 @@ mod tests { state: None, postal_code: None, country: None, - company: Some("Bitwarden".to_string()), + company: Some(DecryptedString::test("Bitwarden")), email: None, phone: None, ssn: None, - username: Some("JDoe".to_string()), + username: Some(DecryptedString::test("JDoe")), passport_number: None, license_number: None, })), diff --git a/crates/bitwarden-exporters/src/lib.rs b/crates/bitwarden-exporters/src/lib.rs index f17d31a2d..49764aa49 100644 --- a/crates/bitwarden-exporters/src/lib.rs +++ b/crates/bitwarden-exporters/src/lib.rs @@ -1,4 +1,4 @@ -use bitwarden_crypto::Kdf; +use bitwarden_crypto::{DecryptedString, Kdf, SensitiveString}; use chrono::{DateTime, Utc}; use thiserror::Error; use uuid::Uuid; @@ -13,7 +13,7 @@ use encrypted_json::export_encrypted_json; pub enum Format { Csv, Json, - EncryptedJson { password: String, kdf: Kdf }, + EncryptedJson { password: SensitiveString, kdf: Kdf }, } /// Export representation of a Bitwarden folder. @@ -22,7 +22,7 @@ pub enum Format { /// that is not tied to the internal vault models. We may revisit this in the future. pub struct Folder { pub id: Uuid, - pub name: String, + pub name: DecryptedString, } /// Export representation of a Bitwarden cipher. @@ -33,8 +33,8 @@ pub struct Cipher { pub id: Uuid, pub folder_id: Option, - pub name: String, - pub notes: Option, + pub name: DecryptedString, + pub notes: Option, pub r#type: CipherType, @@ -50,8 +50,8 @@ pub struct Cipher { #[derive(Clone)] pub struct Field { - pub name: Option, - pub value: Option, + pub name: Option, + pub value: Option, pub r#type: u8, pub linked_id: Option, } @@ -75,24 +75,24 @@ impl ToString for CipherType { } pub struct Login { - pub username: Option, - pub password: Option, + pub username: Option, + pub password: Option, pub login_uris: Vec, - pub totp: Option, + pub totp: Option, } pub struct LoginUri { - pub uri: Option, + pub uri: Option, pub r#match: Option, } pub struct Card { - pub cardholder_name: Option, - pub exp_month: Option, - pub exp_year: Option, - pub code: Option, - pub brand: Option, - pub number: Option, + pub cardholder_name: Option, + pub exp_month: Option, + pub exp_year: Option, + pub code: Option, + pub brand: Option, + pub number: Option, } pub struct SecureNote { @@ -104,24 +104,24 @@ pub enum SecureNoteType { } pub struct Identity { - pub title: Option, - pub first_name: Option, - pub middle_name: Option, - pub last_name: Option, - pub address1: Option, - pub address2: Option, - pub address3: Option, - pub city: Option, - pub state: Option, - pub postal_code: Option, - pub country: Option, - pub company: Option, - pub email: Option, - pub phone: Option, - pub ssn: Option, - pub username: Option, - pub passport_number: Option, - pub license_number: Option, + pub title: Option, + pub first_name: Option, + pub middle_name: Option, + pub last_name: Option, + pub address1: Option, + pub address2: Option, + pub address3: Option, + pub city: Option, + pub state: Option, + pub postal_code: Option, + pub country: Option, + pub company: Option, + pub email: Option, + pub phone: Option, + pub ssn: Option, + pub username: Option, + pub passport_number: Option, + pub license_number: Option, } #[derive(Error, Debug)] diff --git a/crates/bitwarden-generators/Cargo.toml b/crates/bitwarden-generators/Cargo.toml index 1780c37c8..6059642c7 100644 --- a/crates/bitwarden-generators/Cargo.toml +++ b/crates/bitwarden-generators/Cargo.toml @@ -1,10 +1,10 @@ [package] name = "bitwarden-generators" -version = "0.1.0" description = """ Internal crate for the bitwarden crate. Do not use. """ +version.workspace = true authors.workspace = true edition.workspace = true rust-version.workspace = true @@ -20,15 +20,19 @@ mobile = ["dep:uniffi"] # Mobile-specific features bitwarden-crypto = { workspace = true } rand = ">=0.8.5, <0.9" reqwest = { version = ">=0.12, <0.13", features = [ + "http2", "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 } +uniffi = { version = "=0.27.1", optional = true } [dev-dependencies] rand_chacha = "0.3.1" tokio = { version = "1.36.0", features = ["rt", "macros"] } wiremock = "0.6.0" + +[lints] +workspace = true diff --git a/crates/bitwarden-generators/src/username.rs b/crates/bitwarden-generators/src/username.rs index ccb46604b..36ded98b2 100644 --- a/crates/bitwarden-generators/src/username.rs +++ b/crates/bitwarden-generators/src/username.rs @@ -173,7 +173,7 @@ fn random_number(mut rng: impl RngCore) -> String { } /// Generate a username using a plus addressed email address -/// The format is +@ +/// The format is `+@` fn username_subaddress(mut rng: impl RngCore, r#type: AppendType, email: String) -> String { if email.len() < 3 { return email; @@ -195,7 +195,7 @@ fn username_subaddress(mut rng: impl RngCore, r#type: AppendType, email: String) } /// Generate a username using a catchall email address -/// The format is @ +/// The format is `@` fn username_catchall(mut rng: impl RngCore, r#type: AppendType, domain: String) -> String { if domain.is_empty() { return domain; diff --git a/crates/bitwarden-generators/src/username_forwarders/fastmail.rs b/crates/bitwarden-generators/src/username_forwarders/fastmail.rs index 6cc63647a..8a73250c4 100644 --- a/crates/bitwarden-generators/src/username_forwarders/fastmail.rs +++ b/crates/bitwarden-generators/src/username_forwarders/fastmail.rs @@ -34,7 +34,7 @@ pub async fn generate_with_api_url( "new-masked-email": { "state": "enabled", "description": "", - "url": website, + "forDomain": website, "emailPrefix": null, }, }, diff --git a/crates/bitwarden-json/Cargo.toml b/crates/bitwarden-json/Cargo.toml index 16116567b..4981e8f4b 100644 --- a/crates/bitwarden-json/Cargo.toml +++ b/crates/bitwarden-json/Cargo.toml @@ -21,9 +21,11 @@ secrets = ["bitwarden/secrets"] # Secrets manager API [dependencies] async-lock = ">=3.3.0, <4.0" +bitwarden = { workspace = true } log = ">=0.4.18, <0.5" schemars = ">=0.8.12, <0.9" serde = { version = ">=1.0, <2.0", features = ["derive"] } serde_json = ">=1.0.96, <2.0" -bitwarden = { workspace = true } +[lints] +workspace = true diff --git a/crates/bitwarden-json/src/client.rs b/crates/bitwarden-json/src/client.rs index ef9414f12..feb864b72 100644 --- a/crates/bitwarden-json/src/client.rs +++ b/crates/bitwarden-json/src/client.rs @@ -49,15 +49,15 @@ impl Client { match cmd { #[cfg(feature = "internal")] - Command::PasswordLogin(req) => client.auth().login_password(&req).await.into_string(), + Command::PasswordLogin(req) => client.auth().login_password(req).await.into_string(), #[cfg(feature = "secrets")] Command::AccessTokenLogin(req) => { client.auth().login_access_token(&req).await.into_string() } #[cfg(feature = "internal")] - Command::GetUserApiKey(req) => client.get_user_api_key(&req).await.into_string(), + Command::GetUserApiKey(req) => client.get_user_api_key(req).await.into_string(), #[cfg(feature = "internal")] - Command::ApiKeyLogin(req) => client.auth().login_api_key(&req).await.into_string(), + Command::ApiKeyLogin(req) => client.auth().login_api_key(req).await.into_string(), #[cfg(feature = "internal")] Command::Sync(req) => client.sync(&req).await.into_string(), #[cfg(feature = "internal")] diff --git a/crates/bitwarden-json/src/response.rs b/crates/bitwarden-json/src/response.rs index 25c31ef75..b76d97aa6 100644 --- a/crates/bitwarden-json/src/response.rs +++ b/crates/bitwarden-json/src/response.rs @@ -57,7 +57,7 @@ impl ResponseIntoString for Response { Ok(ser) => ser, Err(e) => { let error = Response::error(format!("Failed to serialize Response: {}", e)); - serde_json::to_string(&error).unwrap() + serde_json::to_string(&error).expect("Serialize should be infallible") } } } diff --git a/crates/bitwarden-napi/Cargo.toml b/crates/bitwarden-napi/Cargo.toml index 016401fe7..7bcd54aad 100644 --- a/crates/bitwarden-napi/Cargo.toml +++ b/crates/bitwarden-napi/Cargo.toml @@ -18,17 +18,16 @@ license-file.workspace = true crate-type = ["cdylib", "rlib"] [dependencies] +bitwarden-json = { path = "../bitwarden-json", version = "0.3.0", features = [ + "secrets", +] } env_logger = "0.11.1" log = "0.4.20" napi = { version = "2", features = ["async"] } napi-derive = "2" -bitwarden-json = { path = "../bitwarden-json", version = "0.3.0", features = [ - "secrets", -] } - [build-dependencies] napi-build = "2.1.0" -[profile.release] -lto = true +[lints] +workspace = true diff --git a/crates/bitwarden-napi/README.md b/crates/bitwarden-napi/README.md index d16c8152d..d9e3e7f27 100644 --- a/crates/bitwarden-napi/README.md +++ b/crates/bitwarden-napi/README.md @@ -20,7 +20,7 @@ const accessToken = "-- REDACTED --"; const client = new BitwardenClient(settings, LogLevel.Info); -// Authenticating using a service accounts access token +// Authenticating using a machine account access token const result = await client.loginWithAccessToken(accessToken); if (!result.success) { throw Error("Authentication failed"); diff --git a/crates/bitwarden-napi/package-lock.json b/crates/bitwarden-napi/package-lock.json index c1c3f1a86..49af28702 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.18.0", - "resolved": "https://registry.npmjs.org/@napi-rs/cli/-/cli-2.18.0.tgz", - "integrity": "sha512-lfSRT7cs3iC4L+kv9suGYQEezn5Nii7Kpu+THsYVI0tA1Vh59LH45p4QADaD7hvIkmOz79eEGtoKQ9nAkAPkzA==", + "version": "2.18.2", + "resolved": "https://registry.npmjs.org/@napi-rs/cli/-/cli-2.18.2.tgz", + "integrity": "sha512-IXQji3IF5eStxTHe/PxDSRjPrFymYAQ5FbIvqurxzxyWR8nJql9mtvmCP8y2g8tSoW5xhaToLQW0+mO3lUZq4w==", "dev": true, "bin": { "napi": "scripts/index.js" @@ -71,9 +71,9 @@ } }, "node_modules/@tsconfig/node10": { - "version": "1.0.9", - "resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.9.tgz", - "integrity": "sha512-jNsYVVxU8v5g43Erja32laIDHXeoNvFEpX33OK4d6hljo3jDhCBDhx5dhCCTMWUojscpAagGiRkBKxpdl9fxqA==", + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.11.tgz", + "integrity": "sha512-DcRjDCujK/kCk/cUe8Xz8ZSpm8mS3mNNpta+jGCA6USEDfktlNvm1+IuZ9eTcDbNk41BHwpHHeW+N1lKCz4zOw==", "dev": true }, "node_modules/@tsconfig/node12": { @@ -95,9 +95,9 @@ "dev": true }, "node_modules/@types/node": { - "version": "20.11.19", - "resolved": "https://registry.npmjs.org/@types/node/-/node-20.11.19.tgz", - "integrity": "sha512-7xMnVEcZFu0DikYjWOlRq7NTPETrm7teqUT2WkQjrTIkEgUyyGdWsj/Zg8bEJt5TNklzbPD1X3fqfsHw3SpapQ==", + "version": "20.12.8", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.12.8.tgz", + "integrity": "sha512-NU0rJLJnshZWdE/097cdCBbyW1h4hEg0xpovcoAQYHl8dnEyp/NAOiE45pvc+Bd1Dt+2r94v2eGFpQJ4R7g+2w==", "dev": true, "peer": true, "dependencies": { @@ -196,9 +196,9 @@ } }, "node_modules/typescript": { - "version": "5.3.3", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.3.3.tgz", - "integrity": "sha512-pXWcraxM0uxAS+tN0AG/BF2TyqmHO014Z070UsJ+pFvYuRSq8KH8DmWpnbXe0pEPDHXZV3FcAbJkijJ5oNEnWw==", + "version": "5.4.5", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.4.5.tgz", + "integrity": "sha512-vcI4UpRgg81oIRUFwR0WSIHKt11nJ7SAVlYNIu+QpqeyXP+gpQJy/Z4+F0aGxSE4MqwjyXvW/TzgkLAx2AGHwQ==", "dev": true, "bin": { "tsc": "bin/tsc", diff --git a/crates/bitwarden-py/Cargo.toml b/crates/bitwarden-py/Cargo.toml index f66a0c11f..e9073d802 100644 --- a/crates/bitwarden-py/Cargo.toml +++ b/crates/bitwarden-py/Cargo.toml @@ -16,11 +16,10 @@ name = "bitwarden_py" crate-type = ["cdylib"] [dependencies] +bitwarden-json = { path = "../bitwarden-json", features = ["secrets"] } pyo3 = { version = "0.20.2", features = ["extension-module"] } pyo3-log = "0.9.0" -bitwarden-json = { path = "../bitwarden-json", features = ["secrets"] } - [build-dependencies] pyo3-build-config = { version = "0.20.2" } @@ -30,3 +29,6 @@ pyo3-asyncio = { version = "0.20.0", features = [ "attributes", "tokio-runtime", ] } + +[lints] +workspace = true diff --git a/crates/bitwarden-py/src/client.rs b/crates/bitwarden-py/src/client.rs index f1d282b41..c3ea62444 100644 --- a/crates/bitwarden-py/src/client.rs +++ b/crates/bitwarden-py/src/client.rs @@ -8,7 +8,10 @@ pub struct BitwardenClient(JsonClient); impl BitwardenClient { #[new] pub fn new(settings_string: Option) -> Self { - pyo3_log::init(); + // This will only fail if another logger was already initialized, so we can ignore the + // result + let _ = pyo3_log::try_init(); + Self(JsonClient::new(settings_string)) } diff --git a/crates/bitwarden-uniffi/Cargo.toml b/crates/bitwarden-uniffi/Cargo.toml index 94572b991..8b62ce5c5 100644 --- a/crates/bitwarden-uniffi/Cargo.toml +++ b/crates/bitwarden-uniffi/Cargo.toml @@ -19,17 +19,21 @@ bench = false [dependencies] async-lock = "3.3.0" +async-trait = "0.1.80" +bitwarden = { workspace = true, features = ["mobile", "internal"] } +bitwarden-crypto = { workspace = true, features = ["mobile"] } +bitwarden-generators = { workspace = true, features = ["mobile"] } chrono = { version = ">=0.4.26, <0.5", features = [ "serde", "std", ], default-features = false } env_logger = "0.11.1" schemars = { version = ">=0.8, <0.9", optional = true } -uniffi = "=0.26.1" - -bitwarden = { workspace = true, features = ["mobile", "internal"] } -bitwarden-crypto = { workspace = true, features = ["mobile"] } -bitwarden-generators = { workspace = true, features = ["mobile"] } +uniffi = "=0.27.1" +uuid = ">=1.3.3, <2" [build-dependencies] -uniffi = { version = "=0.26.1", features = ["build"] } +uniffi = { version = "=0.27.1", features = ["build"] } + +[lints] +workspace = true diff --git a/crates/bitwarden-uniffi/README.md b/crates/bitwarden-uniffi/README.md index 1be7706bb..4b2e61714 100644 --- a/crates/bitwarden-uniffi/README.md +++ b/crates/bitwarden-uniffi/README.md @@ -2,10 +2,12 @@ ## Generating documentation +If desired we have some scripts that generates markdown documentation from the rustdoc output. + ```bash cargo +nightly rustdoc -p bitwarden -- -Zunstable-options --output-format json cargo +nightly rustdoc -p bitwarden-uniffi -- -Zunstable-options --output-format json npm run schemas -npx ts-node ./support/docs/docs.ts > languages/kotlin/doc.md +npx ts-node ./support/docs/docs.ts > doc.md ``` diff --git a/crates/bitwarden-uniffi/src/auth/mod.rs b/crates/bitwarden-uniffi/src/auth/mod.rs index 62b709045..8f3903778 100644 --- a/crates/bitwarden-uniffi/src/auth/mod.rs +++ b/crates/bitwarden-uniffi/src/auth/mod.rs @@ -4,7 +4,9 @@ use bitwarden::auth::{ password::MasterPasswordPolicyOptions, AuthRequestResponse, RegisterKeyResponse, RegisterTdeKeyResponse, }; -use bitwarden_crypto::{AsymmetricEncString, HashPurpose, Kdf, TrustDeviceResponse}; +use bitwarden_crypto::{ + AsymmetricEncString, HashPurpose, Kdf, SensitiveString, TrustDeviceResponse, +}; use crate::{error::Result, Client}; @@ -16,7 +18,7 @@ impl ClientAuth { /// **API Draft:** Calculate Password Strength pub async fn password_strength( &self, - password: String, + password: SensitiveString, email: String, additional_inputs: Vec, ) -> u8 { @@ -32,7 +34,7 @@ impl ClientAuth { /// Evaluate if the provided password satisfies the provided policy pub async fn satisfies_policy( &self, - password: String, + password: SensitiveString, strength: u8, policy: MasterPasswordPolicyOptions, ) -> bool { @@ -49,10 +51,10 @@ impl ClientAuth { pub async fn hash_password( &self, email: String, - password: String, + password: SensitiveString, kdf_params: Kdf, purpose: HashPurpose, - ) -> Result { + ) -> Result { Ok(self .0 .0 @@ -67,7 +69,7 @@ impl ClientAuth { pub async fn make_register_keys( &self, email: String, - password: String, + password: SensitiveString, kdf: Kdf, ) -> Result { Ok(self @@ -82,16 +84,15 @@ impl ClientAuth { /// Generate keys needed for TDE process pub async fn make_register_tde_keys( &self, + email: String, org_public_key: String, remember_device: bool, ) -> Result { - Ok(self - .0 - .0 - .write() - .await - .auth() - .make_register_tde_keys(org_public_key, remember_device)?) + Ok(self.0 .0.write().await.auth().make_register_tde_keys( + email, + org_public_key, + remember_device, + )?) } /// Validate the user password @@ -99,14 +100,18 @@ impl ClientAuth { /// To retrieve the user's password hash, use [`ClientAuth::hash_password`] with /// `HashPurpose::LocalAuthentication` during login and persist it. If the login method has no /// password, use the email OTP. - pub async fn validate_password(&self, password: String, password_hash: String) -> Result { + pub async fn validate_password( + &self, + password: SensitiveString, + password_hash: SensitiveString, + ) -> Result { Ok(self .0 .0 .write() .await .auth() - .validate_password(password, password_hash.to_string())?) + .validate_password(password, password_hash)?) } /// Validate the user password without knowing the password hash @@ -117,9 +122,9 @@ impl ClientAuth { /// This works by comparing the provided password against the encrypted user key. pub async fn validate_password_user_key( &self, - password: String, + password: SensitiveString, encrypted_user_key: String, - ) -> Result { + ) -> Result { Ok(self .0 .0 diff --git a/crates/bitwarden-uniffi/src/crypto.rs b/crates/bitwarden-uniffi/src/crypto.rs index 0f877d52e..5907e9a8a 100644 --- a/crates/bitwarden-uniffi/src/crypto.rs +++ b/crates/bitwarden-uniffi/src/crypto.rs @@ -3,7 +3,7 @@ use std::sync::Arc; use bitwarden::mobile::crypto::{ DerivePinKeyResponse, InitOrgCryptoRequest, InitUserCryptoRequest, UpdatePasswordResponse, }; -use bitwarden_crypto::{AsymmetricEncString, EncString}; +use bitwarden_crypto::{AsymmetricEncString, EncString, SensitiveString}; use crate::{error::Result, Client}; @@ -40,7 +40,7 @@ impl ClientCrypto { /// Get the uses's decrypted encryption key. Note: It's very important /// to keep this key safe, as it can be used to decrypt all of the user's data - pub async fn get_user_encryption_key(&self) -> Result { + pub async fn get_user_encryption_key(&self) -> Result { Ok(self .0 .0 @@ -53,7 +53,10 @@ impl ClientCrypto { /// Update the user's password, which will re-encrypt the user's encryption key with the new /// password. This returns the new encrypted user key and the new password hash. - pub async fn update_password(&self, new_password: String) -> Result { + pub async fn update_password( + &self, + new_password: SensitiveString, + ) -> Result { Ok(self .0 .0 @@ -67,7 +70,7 @@ impl ClientCrypto { /// 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 { + pub async fn derive_pin_key(&self, pin: SensitiveString) -> Result { Ok(self.0 .0.write().await.crypto().derive_pin_key(pin).await?) } diff --git a/crates/bitwarden-uniffi/src/error.rs b/crates/bitwarden-uniffi/src/error.rs index 5eef9bbd5..cf07a07a1 100644 --- a/crates/bitwarden-uniffi/src/error.rs +++ b/crates/bitwarden-uniffi/src/error.rs @@ -14,6 +14,23 @@ impl From for BitwardenError { } } +impl From for bitwarden::error::Error { + fn from(val: BitwardenError) -> Self { + match val { + BitwardenError::E(e) => e, + } + } +} + +// Need to implement this From<> impl in order to handle unexpected callback errors. See the +// following page in the Uniffi user guide: +// +impl From for BitwardenError { + fn from(e: uniffi::UnexpectedUniFFICallbackError) -> Self { + Self::E(bitwarden::error::Error::UniffiCallback(e)) + } +} + impl Display for BitwardenError { fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { match self { diff --git a/crates/bitwarden-uniffi/src/platform/fido2.rs b/crates/bitwarden-uniffi/src/platform/fido2.rs new file mode 100644 index 000000000..25756bdab --- /dev/null +++ b/crates/bitwarden-uniffi/src/platform/fido2.rs @@ -0,0 +1,256 @@ +use std::sync::Arc; + +use bitwarden::{ + error::Result as BitResult, + platform::fido2::{ + CheckUserOptions, ClientData, GetAssertionRequest, GetAssertionResult, + MakeCredentialRequest, MakeCredentialResult, + PublicKeyCredentialAuthenticatorAssertionResponse, + PublicKeyCredentialAuthenticatorAttestationResponse, + }, + vault::{Cipher, CipherView, Fido2Credential, Fido2CredentialView}, +}; + +use crate::{error::Result, Client}; + +/// At the moment this is just a stub implementation that doesn't do anything. It's here to make +/// it possible to check the usability API on the native clients. +#[derive(uniffi::Object)] +pub struct ClientFido2(pub(crate) Arc); + +#[uniffi::export] +impl ClientFido2 { + pub fn authenticator( + self: Arc, + user_interface: Arc, + credential_store: Arc, + ) -> Arc { + Arc::new(ClientFido2Authenticator( + self.0.clone(), + user_interface, + credential_store, + )) + } + + pub fn client( + self: Arc, + user_interface: Arc, + credential_store: Arc, + ) -> Arc { + Arc::new(ClientFido2Client(ClientFido2Authenticator( + self.0.clone(), + user_interface, + credential_store, + ))) + } +} + +#[derive(uniffi::Object)] +pub struct ClientFido2Authenticator( + pub(crate) Arc, + pub(crate) Arc, + pub(crate) Arc, +); + +#[uniffi::export] +impl ClientFido2Authenticator { + pub async fn make_credential( + &self, + request: MakeCredentialRequest, + ) -> Result { + let mut client = self.0 .0.write().await; + + let mut platform = client.platform(); + let mut fido2 = platform.fido2(); + let ui = UniffiTraitBridge(self.1.as_ref()); + let cs = UniffiTraitBridge(self.2.as_ref()); + let mut auth = fido2.create_authenticator(&ui, &cs)?; + + let result = auth.make_credential(request).await?; + Ok(result) + } + + pub async fn get_assertion(&self, request: GetAssertionRequest) -> Result { + let mut client = self.0 .0.write().await; + + let mut platform = client.platform(); + let mut fido2 = platform.fido2(); + let ui = UniffiTraitBridge(self.1.as_ref()); + let cs = UniffiTraitBridge(self.2.as_ref()); + let mut auth = fido2.create_authenticator(&ui, &cs)?; + + let result = auth.get_assertion(request).await?; + Ok(result) + } + + pub async fn silently_discover_credentials( + &self, + rp_id: String, + ) -> Result> { + let mut client = self.0 .0.write().await; + + let mut platform = client.platform(); + let mut fido2 = platform.fido2(); + let ui = UniffiTraitBridge(self.1.as_ref()); + let cs = UniffiTraitBridge(self.2.as_ref()); + let mut auth = fido2.create_authenticator(&ui, &cs)?; + + let result = auth.silently_discover_credentials(rp_id).await?; + Ok(result) + } +} + +#[derive(uniffi::Object)] +pub struct ClientFido2Client(pub(crate) ClientFido2Authenticator); + +#[uniffi::export] +impl ClientFido2Client { + pub async fn register( + &self, + origin: String, + request: String, + client_data: ClientData, + ) -> Result { + let mut client = self.0 .0 .0.write().await; + + let mut platform = client.platform(); + let mut fido2 = platform.fido2(); + let ui = UniffiTraitBridge(self.0 .1.as_ref()); + let cs = UniffiTraitBridge(self.0 .2.as_ref()); + let mut client = fido2.create_client(&ui, &cs)?; + + let result = client.register(origin, request, client_data).await?; + Ok(result) + } + + pub async fn authenticate( + &self, + origin: String, + request: String, + client_data: ClientData, + ) -> Result { + let mut client = self.0 .0 .0.write().await; + + let mut platform = client.platform(); + let mut fido2 = platform.fido2(); + let ui = UniffiTraitBridge(self.0 .1.as_ref()); + let cs = UniffiTraitBridge(self.0 .2.as_ref()); + let mut client = fido2.create_client(&ui, &cs)?; + + let result = client.authenticate(origin, request, client_data).await?; + Ok(result) + } +} + +// Note that uniffi doesn't support external traits for now it seems, so we have to duplicate them +// here. + +#[allow(dead_code)] +#[derive(uniffi::Record)] +pub struct CheckUserResult { + user_present: bool, + user_verified: bool, +} + +#[uniffi::export(with_foreign)] +#[async_trait::async_trait] +pub trait UserInterface: Send + Sync { + async fn check_user( + &self, + options: CheckUserOptions, + credential: Option, + ) -> Result; + async fn pick_credential_for_authentication( + &self, + available_credentials: Vec, + ) -> Result; + async fn pick_credential_for_creation( + &self, + available_credentials: Vec, + new_credential: Fido2Credential, + ) -> Result; +} + +#[uniffi::export(with_foreign)] +#[async_trait::async_trait] +pub trait CredentialStore: Send + Sync { + async fn find_credentials( + &self, + ids: Option>>, + rip_id: String, + ) -> Result>; + + async fn save_credential(&self, cred: Cipher) -> Result<()>; +} + +// Because uniffi doesn't support external traits, we have to make a copy of the trait here. +// Ideally we'd want to implement the original trait for every item that implements our local copy, +// but the orphan rules don't allow us to blanket implement an external trait. So we have to wrap +// the trait in a newtype and implement the trait for the newtype. +struct UniffiTraitBridge(T); + +#[async_trait::async_trait] +impl bitwarden::platform::fido2::CredentialStore for UniffiTraitBridge<&dyn CredentialStore> { + async fn find_credentials( + &self, + ids: Option>>, + rip_id: String, + ) -> BitResult> { + self.0 + .find_credentials(ids, rip_id) + .await + .map_err(Into::into) + } + + async fn save_credential(&self, cred: Cipher) -> BitResult<()> { + self.0.save_credential(cred).await.map_err(Into::into) + } +} + +// Uniffi seems to have trouble generating code for Android when a local trait returns a type from +// an external crate. If the type is small we can just copy it over and convert back and forth, but +// Cipher is too big for that to be practical. So we wrap it in a newtype, which is local to the +// trait and so we can sidestep the Uniffi issue +#[derive(uniffi::Record)] +pub struct CipherViewWrapper { + cipher: CipherView, +} + +#[async_trait::async_trait] +impl bitwarden::platform::fido2::UserInterface for UniffiTraitBridge<&dyn UserInterface> { + async fn check_user( + &self, + options: CheckUserOptions, + credential: Option, + ) -> BitResult { + self.0 + .check_user(options, credential) + .await + .map(|r| bitwarden::platform::fido2::CheckUserResult { + user_present: r.user_present, + user_verified: r.user_verified, + }) + .map_err(Into::into) + } + async fn pick_credential_for_authentication( + &self, + available_credentials: Vec, + ) -> BitResult { + self.0 + .pick_credential_for_authentication(available_credentials) + .await + .map(|v| v.cipher) + .map_err(Into::into) + } + async fn pick_credential_for_creation( + &self, + available_credentials: Vec, + new_credential: Fido2Credential, + ) -> BitResult { + self.0 + .pick_credential_for_creation(available_credentials, new_credential) + .await + .map(|v| v.cipher) + .map_err(Into::into) + } +} diff --git a/crates/bitwarden-uniffi/src/platform/mod.rs b/crates/bitwarden-uniffi/src/platform/mod.rs index 33b14d345..458306676 100644 --- a/crates/bitwarden-uniffi/src/platform/mod.rs +++ b/crates/bitwarden-uniffi/src/platform/mod.rs @@ -4,6 +4,8 @@ use bitwarden::platform::FingerprintRequest; use crate::{error::Result, Client}; +mod fido2; + #[derive(uniffi::Object)] pub struct ClientPlatform(pub(crate) Arc); @@ -37,4 +39,9 @@ impl ClientPlatform { self.0 .0.write().await.load_flags(flags); Ok(()) } + + /// FIDO2 operations + pub fn fido2(self: Arc) -> Arc { + Arc::new(fido2::ClientFido2(self.0.clone())) + } } diff --git a/crates/bitwarden-uniffi/src/uniffi_support.rs b/crates/bitwarden-uniffi/src/uniffi_support.rs index 663d5c41e..f487dfaa0 100644 --- a/crates/bitwarden-uniffi/src/uniffi_support.rs +++ b/crates/bitwarden-uniffi/src/uniffi_support.rs @@ -1,7 +1,10 @@ -use bitwarden_crypto::{AsymmetricEncString, EncString}; +use bitwarden_crypto::{AsymmetricEncString, EncString, SensitiveString}; +use uuid::Uuid; // 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); +uniffi::ffi_converter_forward!(SensitiveString, bitwarden::UniFfiTag, crate::UniFfiTag); +uniffi::ffi_converter_forward!(Uuid, bitwarden::UniFfiTag, crate::UniFfiTag); diff --git a/crates/bitwarden-uniffi/src/vault/ciphers.rs b/crates/bitwarden-uniffi/src/vault/ciphers.rs index eb8543947..dcf224a38 100644 --- a/crates/bitwarden-uniffi/src/vault/ciphers.rs +++ b/crates/bitwarden-uniffi/src/vault/ciphers.rs @@ -1,6 +1,7 @@ use std::sync::Arc; use bitwarden::vault::{Cipher, CipherListView, CipherView}; +use uuid::Uuid; use crate::{Client, Result}; @@ -47,4 +48,21 @@ impl ClientCiphers { .decrypt_list(ciphers) .await?) } + + /// Move a cipher to an organization, reencrypting the cipher key if necessary + pub async fn move_to_organization( + &self, + cipher: CipherView, + organization_id: Uuid, + ) -> Result { + Ok(self + .0 + .0 + .read() + .await + .vault() + .ciphers() + .move_to_organization(cipher, organization_id) + .await?) + } } diff --git a/crates/bitwarden-wasm/Cargo.toml b/crates/bitwarden-wasm/Cargo.toml index a688f12b6..6103e55a6 100644 --- a/crates/bitwarden-wasm/Cargo.toml +++ b/crates/bitwarden-wasm/Cargo.toml @@ -15,6 +15,15 @@ keywords.workspace = true crate-type = ["cdylib"] [dependencies] +argon2 = { version = ">=0.5.0, <0.6", features = [ + "alloc", + "zeroize", +], default-features = false } +bitwarden-crypto = { workspace = true } +bitwarden-json = { path = "../bitwarden-json", features = [ + "secrets", + "internal", +] } console_error_panic_hook = "0.1.7" console_log = { version = "1.0.0", features = ["color"] } js-sys = "0.3.68" @@ -23,10 +32,8 @@ serde = { version = "1.0.196", features = ["derive"] } wasm-bindgen = { version = "0.2.91", features = ["serde-serialize"] } wasm-bindgen-futures = "0.4.41" -bitwarden-json = { path = "../bitwarden-json", features = [ - "secrets", - "internal", -] } - [dev-dependencies] wasm-bindgen-test = "0.3.41" + +[lints] +workspace = true diff --git a/crates/bitwarden-wasm/src/client.rs b/crates/bitwarden-wasm/src/client.rs index 542759731..240e10c47 100644 --- a/crates/bitwarden-wasm/src/client.rs +++ b/crates/bitwarden-wasm/src/client.rs @@ -1,6 +1,8 @@ extern crate console_error_panic_hook; use std::rc::Rc; +use argon2::{Algorithm, Argon2, Params, Version}; +use bitwarden_crypto::SensitiveVec; use bitwarden_json::client::Client as JsonClient; use js_sys::Promise; use log::Level; @@ -54,3 +56,30 @@ impl BitwardenClient { }) } } + +#[wasm_bindgen] +pub fn argon2( + password: Vec, + salt: Vec, + iterations: u32, + memory: u32, + parallelism: u32, +) -> Result, JsError> { + let password = SensitiveVec::new(Box::new(password)); + let salt = SensitiveVec::new(Box::new(salt)); + + let argon = Argon2::new( + Algorithm::Argon2id, + Version::V0x13, + Params::new( + memory * 1024, // Convert MiB to KiB + iterations, + parallelism, + Some(32), + )?, + ); + + let mut hash = [0u8; 32]; + argon.hash_password_into(password.expose(), salt.expose(), &mut hash)?; + Ok(hash.to_vec()) +} diff --git a/crates/bitwarden/CHANGELOG.md b/crates/bitwarden/CHANGELOG.md index ee4c6033b..9496fac8e 100644 --- a/crates/bitwarden/CHANGELOG.md +++ b/crates/bitwarden/CHANGELOG.md @@ -7,11 +7,17 @@ adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). ## [Unreleased] +## [0.5.0] - 2024-04-26 + ### Changed - Switched TLS backend to `rustls`, removing the dependency on `OpenSSL`. (#374) - `client::AccessToken` is now `auth::AccessToken`. (#656) +### Fixed + +- Fix renew for service account access token logins (#702) + ## [0.4.0] - 2023-12-21 ### Added diff --git a/crates/bitwarden/Cargo.toml b/crates/bitwarden/Cargo.toml index 4c9596b81..8652af096 100644 --- a/crates/bitwarden/Cargo.toml +++ b/crates/bitwarden/Cargo.toml @@ -1,11 +1,11 @@ [package] name = "bitwarden" -version = "0.4.0" description = """ Bitwarden Secrets Manager SDK """ keywords = ["bitwarden", "secrets-manager"] +version.workspace = true authors.workspace = true edition.workspace = true rust-version.workspace = true @@ -31,7 +31,8 @@ mobile = [ wasm-bindgen = ["chrono/wasmbind"] [dependencies] -base64 = ">=0.21.2, <0.22" +async-trait = ">=0.1.80, <0.2" +base64 = ">=0.21.2, <0.23" bitwarden-api-api = { workspace = true } bitwarden-api-identity = { workspace = true } bitwarden-crypto = { workspace = true } @@ -46,19 +47,21 @@ chrono = { version = ">=0.4.26, <0.5", features = [ getrandom = { version = ">=0.2.9, <0.3", features = ["js"] } hmac = ">=0.12.1, <0.13" log = ">=0.4.18, <0.5" +passkey = { git = "https://github.com/bitwarden/passkey-rs", rev = "12da886102707f87ad97e499c857c0857ece0b85" } rand = ">=0.8.5, <0.9" reqwest = { version = ">=0.12, <0.13", features = [ + "http2", "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" -serde_qs = ">=0.12.0, <0.13" +serde_qs = ">=0.12.0, <0.14" serde_repr = ">=0.1.12, <0.2" sha1 = ">=0.10.5, <0.11" sha2 = ">=0.10.6, <0.11" thiserror = ">=1.0.40, <2.0" -uniffi = { version = "=0.26.1", optional = true, features = ["tokio"] } +uniffi = { version = "=0.27.1", optional = true, features = ["tokio"] } uuid = { version = ">=1.3.3, <2.0", features = ["serde"] } zxcvbn = ">= 2.2.2, <3.0" @@ -67,7 +70,7 @@ zxcvbn = ">= 2.2.2, <3.0" # There are a few exceptions to this: # - WASM doesn't require a TLS stack, as it just uses the browsers/node fetch # - Android uses webpki-roots for the moment -reqwest = { version = "*", features = [ +reqwest = { version = ">=0.12, <0.13", features = [ "rustls-tls-manual-roots", ], default-features = false } rustls-platform-verifier = "0.2.0" @@ -75,12 +78,16 @@ rustls-platform-verifier = "0.2.0" [target.'cfg(target_os = "android")'.dependencies] # On android, the use of rustls-platform-verifier is more complicated and going through some changes at the moment, so we fall back to using webpki-roots # This means that for the moment android won't support self-signed certificates, even if they are included in the OS trust store -reqwest = { version = "*", features = [ +reqwest = { version = ">=0.12, <0.13", features = [ "rustls-tls-webpki-roots", ], default-features = false } [dev-dependencies] +bitwarden-crypto = { workspace = true, features = ["test"] } rand_chacha = "0.3.1" tokio = { version = "1.36.0", features = ["rt", "macros"] } wiremock = "0.6.0" zeroize = { version = ">=1.7.0, <2.0", features = ["derive", "aarch64"] } + +[lints] +workspace = true diff --git a/crates/bitwarden/src/admin_console/policy.rs b/crates/bitwarden/src/admin_console/policy.rs index 7e87b8623..d8ed0b761 100644 --- a/crates/bitwarden/src/admin_console/policy.rs +++ b/crates/bitwarden/src/admin_console/policy.rs @@ -6,7 +6,7 @@ use serde::{Deserialize, Serialize}; use serde_repr::{Deserialize_repr, Serialize_repr}; use uuid::Uuid; -use crate::error::{Error, Result}; +use crate::error::{require, Error, Result}; #[derive(Serialize, Deserialize, Debug, JsonSchema)] pub struct Policy { @@ -41,11 +41,11 @@ impl TryFrom for Policy { fn try_from(policy: PolicyResponseModel) -> Result { Ok(Self { - id: policy.id.ok_or(Error::MissingFields)?, - organization_id: policy.organization_id.ok_or(Error::MissingFields)?, - r#type: policy.r#type.ok_or(Error::MissingFields)?.into(), + id: require!(policy.id), + organization_id: require!(policy.organization_id), + r#type: require!(policy.r#type).into(), data: policy.data, - enabled: policy.enabled.ok_or(Error::MissingFields)?, + enabled: require!(policy.enabled), }) } } diff --git a/crates/bitwarden/src/auth/access_token.rs b/crates/bitwarden/src/auth/access_token.rs index 667a78529..e074b4524 100644 --- a/crates/bitwarden/src/auth/access_token.rs +++ b/crates/bitwarden/src/auth/access_token.rs @@ -1,7 +1,7 @@ use std::{fmt::Debug, str::FromStr}; use base64::Engine; -use bitwarden_crypto::{derive_shareable_key, SymmetricCryptoKey}; +use bitwarden_crypto::{derive_shareable_key, Sensitive, SymmetricCryptoKey}; use uuid::Uuid; use crate::{error::AccessTokenInvalidError, util::STANDARD_INDIFFERENT}; @@ -45,12 +45,12 @@ impl FromStr for AccessToken { let encryption_key = STANDARD_INDIFFERENT .decode(encryption_key) .map_err(AccessTokenInvalidError::InvalidBase64)?; - let encryption_key: [u8; 16] = encryption_key.try_into().map_err(|e: Vec<_>| { + let encryption_key = Sensitive::new(encryption_key.try_into().map_err(|e: Vec<_>| { AccessTokenInvalidError::InvalidBase64Length { expected: 16, got: e.len(), } - })?; + })?); let encryption_key = derive_shareable_key(encryption_key, "accesstoken", Some("sm-access-token")); diff --git a/crates/bitwarden/src/auth/api/request/mod.rs b/crates/bitwarden/src/auth/api/request/mod.rs index 2b5bde225..263d28357 100644 --- a/crates/bitwarden/src/auth/api/request/mod.rs +++ b/crates/bitwarden/src/auth/api/request/mod.rs @@ -54,7 +54,7 @@ async fn send_identity_connect_request( } let response = request - .body(serde_qs::to_string(&body).unwrap()) + .body(serde_qs::to_string(&body).expect("Serialize should be infallible")) .send() .await?; diff --git a/crates/bitwarden/src/auth/api/response/identity_success_response.rs b/crates/bitwarden/src/auth/api/response/identity_success_response.rs index fb59c1caa..94ebe9445 100644 --- a/crates/bitwarden/src/auth/api/response/identity_success_response.rs +++ b/crates/bitwarden/src/auth/api/response/identity_success_response.rs @@ -22,7 +22,7 @@ pub struct IdentityTokenSuccessResponse { #[serde( rename = "kdfIterations", alias = "KdfIterations", - default = "crate::util::default_pbkdf2_iterations" + default = "bitwarden_crypto::default_pbkdf2_iterations" )] kdf_iterations: NonZeroU32, @@ -41,6 +41,8 @@ pub struct IdentityTokenSuccessResponse { #[cfg(test)] mod test { + use bitwarden_crypto::default_pbkdf2_iterations; + use super::*; impl Default for IdentityTokenSuccessResponse { @@ -54,7 +56,7 @@ mod test { key: Default::default(), two_factor_token: Default::default(), kdf: KdfType::default(), - kdf_iterations: crate::util::default_pbkdf2_iterations(), + kdf_iterations: default_pbkdf2_iterations(), reset_master_password: Default::default(), force_password_reset: Default::default(), api_use_key_connector: Default::default(), diff --git a/crates/bitwarden/src/auth/auth_request.rs b/crates/bitwarden/src/auth/auth_request.rs index 18c71afba..ca47ad579 100644 --- a/crates/bitwarden/src/auth/auth_request.rs +++ b/crates/bitwarden/src/auth/auth_request.rs @@ -1,6 +1,7 @@ use base64::{engine::general_purpose::STANDARD, Engine}; use bitwarden_crypto::{ fingerprint, AsymmetricCryptoKey, AsymmetricEncString, AsymmetricPublicCryptoKey, + SensitiveString, }; #[cfg(feature = "mobile")] use bitwarden_crypto::{EncString, KeyDecryptable, SymmetricCryptoKey}; @@ -12,7 +13,7 @@ use crate::{error::Error, Client}; 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, + pub private_key: SensitiveString, /// Base64 encoded public key pub public_key: String, /// Fingerprint of the public key @@ -34,10 +35,10 @@ pub(crate) fn new_auth_request(email: &str) -> Result Result Result { - let key = AsymmetricCryptoKey::from_der(&STANDARD.decode(private_key)?)?; - let mut key: Vec = user_key.decrypt_with_key(&key)?; + use bitwarden_crypto::DecryptedVec; - Ok(SymmetricCryptoKey::try_from(key.as_mut_slice())?) + let key = AsymmetricCryptoKey::from_der(private_key.decode_base64(STANDARD)?)?; + let key: DecryptedVec = user_key.decrypt_with_key(&key)?; + + Ok(SymmetricCryptoKey::try_from(key)?) } /// Decrypt the user key using the private key generated previously. #[cfg(feature = "mobile")] pub(crate) fn auth_request_decrypt_master_key( - private_key: String, + private_key: SensitiveString, master_key: AsymmetricEncString, user_key: EncString, ) -> Result { - use bitwarden_crypto::MasterKey; + use bitwarden_crypto::{DecryptedVec, MasterKey}; - let key = AsymmetricCryptoKey::from_der(&STANDARD.decode(private_key)?)?; - let mut master_key: Vec = master_key.decrypt_with_key(&key)?; - let master_key = MasterKey::new(SymmetricCryptoKey::try_from(master_key.as_mut_slice())?); + let key = AsymmetricCryptoKey::from_der(private_key.decode_base64(STANDARD)?)?; + let master_key: DecryptedVec = master_key.decrypt_with_key(&key)?; + let master_key = MasterKey::new(SymmetricCryptoKey::try_from(master_key)?); Ok(master_key.decrypt_user_key(user_key)?) } @@ -92,61 +95,61 @@ pub(crate) fn approve_auth_request( let key = enc.get_key(&None).ok_or(Error::VaultLocked)?; Ok(AsymmetricEncString::encrypt_rsa2048_oaep_sha1( - &key.to_vec(), + key.to_vec(), &public_key, )?) } #[test] fn test_auth_request() { - use zeroize::Zeroizing; - let request = new_auth_request("test@bitwarden.com").unwrap(); - let secret: &[u8] = &[ + let secret = bitwarden_crypto::SensitiveVec::test(&[ 111, 32, 97, 169, 4, 241, 174, 74, 239, 206, 113, 86, 174, 68, 216, 238, 52, 85, 156, 27, 134, 149, 54, 55, 91, 147, 45, 130, 131, 237, 51, 31, 191, 106, 155, 14, 160, 82, 47, 40, 96, 31, 114, 127, 212, 187, 167, 110, 205, 116, 198, 243, 218, 72, 137, 53, 248, 43, 255, 67, 35, 61, 245, 93, - ]; + ]); let private_key = - AsymmetricCryptoKey::from_der(&STANDARD.decode(&request.private_key).unwrap()).unwrap(); + AsymmetricCryptoKey::from_der(request.private_key.clone().decode_base64(STANDARD).unwrap()) + .unwrap(); - let encrypted = AsymmetricEncString::encrypt_rsa2048_oaep_sha1(secret, &private_key).unwrap(); + let encrypted = + AsymmetricEncString::encrypt_rsa2048_oaep_sha1(secret.clone(), &private_key).unwrap(); let decrypted = auth_request_decrypt_user_key(request.private_key, encrypted).unwrap(); - assert_eq!(decrypted.to_vec(), Zeroizing::new(secret.to_owned())); + assert_eq!(decrypted.to_vec(), secret); } #[cfg(test)] mod tests { use std::num::NonZeroU32; - use bitwarden_crypto::Kdf; + use base64::Engine; + use bitwarden_crypto::{Kdf, SensitiveVec}; use super::*; - use crate::{ - client::{LoginMethod, UserLoginMethod}, - mobile::crypto::{AuthRequestMethod, InitUserCryptoMethod, InitUserCryptoRequest}, - }; + use crate::mobile::crypto::{AuthRequestMethod, InitUserCryptoMethod, InitUserCryptoRequest}; #[test] fn test_approve() { let mut client = Client::new(None); - client.set_login_method(LoginMethod::User(UserLoginMethod::Username { - client_id: "7b821276-e27c-400b-9853-606393c87f18".to_owned(), - email: "test@bitwarden.com".to_owned(), - kdf: Kdf::PBKDF2 { + + let master_key = bitwarden_crypto::MasterKey::derive( + &SensitiveVec::test(b"asdfasdfasdf"), + "test@bitwarden.com".as_bytes(), + &Kdf::PBKDF2 { iterations: NonZeroU32::new(600_000).unwrap(), }, - })); + ) + .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) + .initialize_user_crypto_master_key(master_key, user_key, private_key) .unwrap(); let public_key = "MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAvyLRDUwXB4BfQ507D4meFPmwn5zwy3IqTPJO4plrrhnclWahXa240BzyFW9gHgYu+Jrgms5xBfRTBMcEsqqNm7+JpB6C1B6yvnik0DpJgWQw1rwvy4SUYidpR/AWbQi47n/hvnmzI/sQxGddVfvWu1iTKOlf5blbKYAXnUE5DZBGnrWfacNXwRRdtP06tFB0LwDgw+91CeLSJ9py6dm1qX5JIxoO8StJOQl65goLCdrTWlox+0Jh4xFUfCkb+s3px+OhSCzJbvG/hlrSRcUz5GnwlCEyF3v5lfUtV96MJD+78d8pmH6CfFAp2wxKRAbGdk+JccJYO6y6oIXd3Fm7twIDAQAB"; @@ -164,14 +167,19 @@ mod tests { let private_key = "MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQCzLtEUdxfcLxDj84yaGFsVF5hZ8Hjlb08NMQDy1RnBma06I3ZESshLYzVz4r/gegMn9OOltfV/Yxlyvida8oW6qdlfJ7AVz6Oa8pV7BiL40C7b76+oqraQpyYw2HChANB1AhXL9SqWngKmLZwjA7qiCrmcc0kZHeOb4KnKtp9iVvPVs+8veFvKgYO4ba2AAOHKFdR0W55/agXfAy+fWUAkC8mc9ikyJdQWaPV6OZvC2XFkOseBQm9Rynudh3BQpoWiL6w620efe7t5k+02/EyOFJL9f/XEEjM/+Yo0t3LAfkuhHGeKiRST59Xc9hTEmyJTeVXROtz+0fjqOp3xkaObAgMBAAECggEACs4xhnO0HaZhh1/iH7zORMIRXKeyxP2LQiTR8xwN5JJ9wRWmGAR9VasS7EZFTDidIGVME2u/h4s5EqXnhxfO+0gGksVvgNXJ/qw87E8K2216g6ZNo6vSGA7H1GH2voWwejJ4/k/cJug6dz2S402rRAKh2Wong1arYHSkVlQp3diiMa5FHAOSE+Cy09O2ZsaF9IXQYUtlW6AVXFrBEPYH2kvkaPXchh8VETMijo6tbvoKLnUHe+wTaDMls7hy8exjtVyI59r3DNzjy1lNGaGb5QSnFMXR+eHhPZc844Wv02MxC15zKABADrl58gpJyjTl6XpDdHCYGsmGpVGH3X9TQQKBgQDz/9beFjzq59ve6rGwn+EtnQfSsyYT+jr7GN8lNEXb3YOFXBgPhfFIcHRh2R00Vm9w2ApfAx2cd8xm2I6HuvQ1Os7g26LWazvuWY0Qzb+KaCLQTEGH1RnTq6CCG+BTRq/a3J8M4t38GV5TWlzv8wr9U4dl6FR4efjb65HXs1GQ4QKBgQC7/uHfrOTEHrLeIeqEuSl0vWNqEotFKdKLV6xpOvNuxDGbgW4/r/zaxDqt0YBOXmRbQYSEhmO3oy9J6XfE1SUln0gbavZeW0HESCAmUIC88bDnspUwS9RxauqT5aF8ODKN/bNCWCnBM1xyonPOs1oT1nyparJVdQoG//Y7vkB3+wKBgBqLqPq8fKAp3XfhHLfUjREDVoiLyQa/YI9U42IOz9LdxKNLo6p8rgVthpvmnRDGnpUuS+KOWjhdqDVANjF6G3t3DG7WNl8Rh5Gk2H4NhFswfSkgQrjebFLlBy9gjQVCWXt8KSmjvPbiY6q52Aaa8IUjA0YJAregvXxfopxO+/7BAoGARicvEtDp7WWnSc1OPoj6N14VIxgYcI7SyrzE0d/1x3ffKzB5e7qomNpxKzvqrVP8DzG7ydh8jaKPmv1MfF8tpYRy3AhmN3/GYwCnPqT75YYrhcrWcVdax5gmQVqHkFtIQkRSCIftzPLlpMGKha/YBV8c1fvC4LD0NPh/Ynv0gtECgYEAyOZg95/kte0jpgUEgwuMrzkhY/AaUJULFuR5MkyvReEbtSBQwV5tx60+T95PHNiFooWWVXiLMsAgyI2IbkxVR1Pzdri3gWK5CTfqb7kLuaj/B7SGvBa2Sxo478KS5K8tBBBWkITqo+wLC0mn3uZi1dyMWO1zopTA+KtEGF2dtGQ="; let enc_user_key = "4.dxbd5OMwi/Avy7DQxvLV+Z7kDJgHBtg/jAbgYNO7QU0Zii4rLFNco2lS5aS9z42LTZHc2p5HYwn2ZwkZNfHsQ6//d5q40MDgGYJMKBXOZP62ZHhct1XsvYBmtcUtIOm5j2HSjt2pjEuGAc1LbyGIWRJJQ3Lp1ULbL2m71I+P23GF36JyOM8SUWvpvxE/3+qqVhRFPG2VqMCYa2kLLxwVfUmpV+KKjX1TXsrq6pfJIwHNwHw4h7MSfD8xTy2bx4MiBt638Z9Vt1pGsSQkh9RgPvCbnhuCpZQloUgJ8ByLVEcrlKx3yaaxiQXvte+ZhuOI7rGdjmoVoOzisooje4JgYw==".parse().unwrap(); - let dec = auth_request_decrypt_user_key(private_key.to_owned(), enc_user_key).unwrap(); + let dec = auth_request_decrypt_user_key( + SensitiveString::new(Box::new(private_key.to_owned())), + enc_user_key, + ) + .unwrap(); assert_eq!( - dec.to_vec().as_ref(), - vec![ + dec.to_vec(), + [ 201, 37, 234, 213, 21, 75, 40, 70, 149, 213, 234, 16, 19, 251, 162, 245, 161, 74, 34, 245, 211, 151, 211, 192, 95, 10, 117, 50, 88, 223, 23, 157 ] + .as_slice() ); } @@ -181,18 +189,22 @@ mod tests { let enc_master_key = "4.dxbd5OMwi/Avy7DQxvLV+Z7kDJgHBtg/jAbgYNO7QU0Zii4rLFNco2lS5aS9z42LTZHc2p5HYwn2ZwkZNfHsQ6//d5q40MDgGYJMKBXOZP62ZHhct1XsvYBmtcUtIOm5j2HSjt2pjEuGAc1LbyGIWRJJQ3Lp1ULbL2m71I+P23GF36JyOM8SUWvpvxE/3+qqVhRFPG2VqMCYa2kLLxwVfUmpV+KKjX1TXsrq6pfJIwHNwHw4h7MSfD8xTy2bx4MiBt638Z9Vt1pGsSQkh9RgPvCbnhuCpZQloUgJ8ByLVEcrlKx3yaaxiQXvte+ZhuOI7rGdjmoVoOzisooje4JgYw==".parse().unwrap(); let enc_user_key = "2.Q/2PhzcC7GdeiMHhWguYAQ==|GpqzVdr0go0ug5cZh1n+uixeBC3oC90CIe0hd/HWA/pTRDZ8ane4fmsEIcuc8eMKUt55Y2q/fbNzsYu41YTZzzsJUSeqVjT8/iTQtgnNdpo=|dwI+uyvZ1h/iZ03VQ+/wrGEFYVewBUUl/syYgjsNMbE=".parse().unwrap(); - let dec = - auth_request_decrypt_master_key(private_key.to_owned(), enc_master_key, enc_user_key) - .unwrap(); + let dec = auth_request_decrypt_master_key( + SensitiveString::new(Box::new(private_key.to_owned())), + enc_master_key, + enc_user_key, + ) + .unwrap(); assert_eq!( - dec.to_vec().as_ref(), - vec![ + dec.to_vec(), + [ 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, ] + .as_slice() ); } @@ -208,14 +220,16 @@ mod tests { // Initialize an existing client which is unlocked let mut existing_device = Client::new(None); - existing_device.set_login_method(LoginMethod::User(UserLoginMethod::Username { - client_id: "123".to_owned(), - email: email.to_owned(), - kdf: kdf.clone(), - })); + + let master_key = bitwarden_crypto::MasterKey::derive( + &SensitiveVec::test(b"asdfasdfasdf"), + email.as_bytes(), + &kdf, + ) + .unwrap(); existing_device - .initialize_user_crypto("asdfasdfasdf", user_key, private_key.parse().unwrap()) + .initialize_user_crypto_master_key(master_key, user_key, private_key.parse().unwrap()) .unwrap(); // Initialize a new device which will request to be logged in diff --git a/crates/bitwarden/src/auth/client_auth.rs b/crates/bitwarden/src/auth/client_auth.rs index 5029d389b..2eae8619f 100644 --- a/crates/bitwarden/src/auth/client_auth.rs +++ b/crates/bitwarden/src/auth/client_auth.rs @@ -1,5 +1,5 @@ #[cfg(feature = "internal")] -use bitwarden_crypto::{AsymmetricEncString, DeviceKey, TrustDeviceResponse}; +use bitwarden_crypto::{AsymmetricEncString, DeviceKey, SensitiveString, TrustDeviceResponse}; #[cfg(feature = "mobile")] use crate::auth::login::NewAuthRequestResponse; @@ -49,7 +49,7 @@ impl<'a> ClientAuth<'a> { impl<'a> ClientAuth<'a> { pub async fn password_strength( &self, - password: String, + password: SensitiveString, email: String, additional_inputs: Vec, ) -> u8 { @@ -58,7 +58,7 @@ impl<'a> ClientAuth<'a> { pub async fn satisfies_policy( &self, - password: String, + password: SensitiveString, strength: u8, policy: &MasterPasswordPolicyOptions, ) -> bool { @@ -68,7 +68,7 @@ impl<'a> ClientAuth<'a> { pub fn make_register_keys( &self, email: String, - password: String, + password: SensitiveString, kdf: Kdf, ) -> Result { make_register_keys(email, password, kdf) @@ -76,13 +76,14 @@ impl<'a> ClientAuth<'a> { pub fn make_register_tde_keys( &mut self, + email: String, org_public_key: String, remember_device: bool, ) -> Result { - make_register_tde_keys(self.client, org_public_key, remember_device) + make_register_tde_keys(self.client, email, org_public_key, remember_device) } - pub async fn register(&mut self, input: &RegisterRequest) -> Result<()> { + pub async fn register(&mut self, input: RegisterRequest) -> Result<()> { register(self.client, input).await } @@ -95,31 +96,35 @@ impl<'a> ClientAuth<'a> { pub async fn login_password( &mut self, - input: &PasswordLoginRequest, + input: PasswordLoginRequest, ) -> Result { login_password(self.client, input).await } pub async fn login_api_key( &mut self, - input: &ApiKeyLoginRequest, + input: ApiKeyLoginRequest, ) -> Result { login_api_key(self.client, input).await } - pub async fn send_two_factor_email(&mut self, tf: &TwoFactorEmailRequest) -> Result<()> { + pub async fn send_two_factor_email(&mut self, tf: TwoFactorEmailRequest) -> Result<()> { send_two_factor_email(self.client, tf).await } - pub fn validate_password(&self, password: String, password_hash: String) -> Result { + pub fn validate_password( + &self, + password: SensitiveString, + password_hash: SensitiveString, + ) -> Result { validate_password(self.client, password, password_hash) } pub fn validate_password_user_key( &self, - password: String, + password: SensitiveString, encrypted_user_key: String, - ) -> Result { + ) -> Result { validate_password_user_key(self.client, password, encrypted_user_key) } diff --git a/crates/bitwarden/src/auth/login/access_token.rs b/crates/bitwarden/src/auth/login/access_token.rs index 46c3f2b50..f95dda52a 100644 --- a/crates/bitwarden/src/auth/login/access_token.rs +++ b/crates/bitwarden/src/auth/login/access_token.rs @@ -1,7 +1,9 @@ use std::path::{Path, PathBuf}; -use base64::{engine::general_purpose::STANDARD, Engine}; -use bitwarden_crypto::{EncString, KeyDecryptable, SymmetricCryptoKey}; +use base64::engine::general_purpose::STANDARD; +use bitwarden_crypto::{ + DecryptedVec, EncString, KeyDecryptable, SensitiveString, SymmetricCryptoKey, +}; use chrono::Utc; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; @@ -14,7 +16,7 @@ use crate::{ AccessToken, JWTToken, }, client::{LoginMethod, ServiceAccountLoginMethod}, - error::{Error, Result}, + error::{require, Error, Result}, secrets_manager::state::{self, ClientState}, Client, }; @@ -53,25 +55,24 @@ pub(crate) async fn login_access_token( // Extract the encrypted payload and use the access token encryption key to decrypt it let payload: EncString = r.encrypted_payload.parse()?; - let decrypted_payload: Vec = payload.decrypt_with_key(&access_token.encryption_key)?; + let decrypted_payload: DecryptedVec = + payload.decrypt_with_key(&access_token.encryption_key)?; // Once decrypted, we have to JSON decode to extract the organization encryption key #[derive(serde::Deserialize)] struct Payload { #[serde(rename = "encryptionKey")] - encryption_key: String, + encryption_key: SensitiveString, } - let payload: Payload = serde_json::from_slice(&decrypted_payload)?; - let mut encryption_key = STANDARD.decode(payload.encryption_key.clone())?; - let encryption_key = SymmetricCryptoKey::try_from(encryption_key.as_mut_slice())?; + let payload: Payload = serde_json::from_slice(decrypted_payload.expose())?; + let encryption_key = payload.encryption_key.clone().decode_base64(STANDARD)?; + let encryption_key = SymmetricCryptoKey::try_from(encryption_key)?; let access_token_obj: JWTToken = r.access_token.parse()?; // This should always be Some() when logging in with an access token - let organization_id = access_token_obj - .organization - .ok_or(Error::MissingFields)? + let organization_id = require!(access_token_obj.organization) .parse() .map_err(|_| Error::InvalidResponse)?; @@ -125,7 +126,7 @@ fn load_tokens_from_state( let organization_id: Uuid = organization_id .parse() .map_err(|_| "Bad organization id.")?; - let encryption_key: SymmetricCryptoKey = client_state.encryption_key.parse()?; + let encryption_key = SymmetricCryptoKey::try_from(client_state.encryption_key)?; client.set_tokens(client_state.token, None, time_till_expiration as u64); client.initialize_crypto_single_key(encryption_key); diff --git a/crates/bitwarden/src/auth/login/api_key.rs b/crates/bitwarden/src/auth/login/api_key.rs index 72d05897a..807457a53 100644 --- a/crates/bitwarden/src/auth/login/api_key.rs +++ b/crates/bitwarden/src/auth/login/api_key.rs @@ -1,4 +1,4 @@ -use bitwarden_crypto::EncString; +use bitwarden_crypto::{EncString, MasterKey, SensitiveString}; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; @@ -9,18 +9,18 @@ use crate::{ JWTToken, }, client::{LoginMethod, UserLoginMethod}, - error::{Error, Result}, + error::{require, Result}, Client, }; pub(crate) async fn login_api_key( client: &mut Client, - input: &ApiKeyLoginRequest, + input: ApiKeyLoginRequest, ) -> Result { //info!("api key logging in"); //debug!("{:#?}, {:#?}", client, input); - let response = request_api_identity_tokens(client, input).await?; + let response = request_api_identity_tokens(client, &input).await?; if let IdentityTokenResponse::Authenticated(r) = &response { let access_token_obj: JWTToken = r.access_token.parse()?; @@ -37,6 +37,9 @@ pub(crate) async fn login_api_key( r.refresh_token.clone(), r.expires_in, ); + + let master_key = MasterKey::derive(&input.password.into(), email.as_bytes(), &kdf)?; + client.set_login_method(LoginMethod::User(UserLoginMethod::ApiKey { client_id: input.client_id.to_owned(), client_secret: input.client_secret.to_owned(), @@ -44,14 +47,10 @@ pub(crate) async fn login_api_key( kdf, })); - let user_key: EncString = r.key.as_deref().ok_or(Error::MissingFields)?.parse()?; - let private_key: EncString = r - .private_key - .as_deref() - .ok_or(Error::MissingFields)? - .parse()?; + let user_key: EncString = require!(r.key.as_deref()).parse()?; + let private_key: EncString = require!(r.private_key.as_deref()).parse()?; - client.initialize_user_crypto(&input.password, user_key, private_key)?; + client.initialize_user_crypto_master_key(master_key, user_key, private_key)?; } ApiKeyLoginResponse::process_response(response) @@ -77,7 +76,7 @@ pub struct ApiKeyLoginRequest { pub client_secret: String, /// Bitwarden account master password - pub password: String, + pub password: SensitiveString, } #[derive(Serialize, Deserialize, Debug, JsonSchema)] diff --git a/crates/bitwarden/src/auth/login/auth_request.rs b/crates/bitwarden/src/auth/login/auth_request.rs index ee2aef254..4c0d250fc 100644 --- a/crates/bitwarden/src/auth/login/auth_request.rs +++ b/crates/bitwarden/src/auth/login/auth_request.rs @@ -1,10 +1,8 @@ -use std::num::NonZeroU32; - use bitwarden_api_api::{ apis::auth_requests_api::{auth_requests_id_response_get, auth_requests_post}, models::{AuthRequestCreateRequestModel, AuthRequestType}, }; -use bitwarden_crypto::Kdf; +use bitwarden_crypto::{Kdf, SensitiveString}; use uuid::Uuid; use crate::{ @@ -13,7 +11,7 @@ use crate::{ auth_request::new_auth_request, }, client::{LoginMethod, UserLoginMethod}, - error::{Error, Result}, + error::{require, Result}, mobile::crypto::{AuthRequestMethod, InitUserCryptoMethod, InitUserCryptoRequest}, Client, }; @@ -24,7 +22,7 @@ pub struct NewAuthRequestResponse { device_identifier: String, auth_request_id: Uuid, access_code: String, - private_key: String, + private_key: SensitiveString, } pub(crate) async fn send_new_auth_request( @@ -50,7 +48,7 @@ pub(crate) async fn send_new_auth_request( fingerprint: auth.fingerprint, email, device_identifier, - auth_request_id: res.id.ok_or(Error::MissingFields)?, + auth_request_id: require!(res.id), access_code: auth.access_code, private_key: auth.private_key, }) @@ -86,9 +84,7 @@ pub(crate) async fn complete_auth_request( .await?; if let IdentityTokenResponse::Authenticated(r) = response { - let kdf = Kdf::PBKDF2 { - iterations: NonZeroU32::new(600_000).unwrap(), - }; + let kdf = Kdf::default(); client.set_tokens( r.access_token.clone(), @@ -103,11 +99,11 @@ pub(crate) async fn complete_auth_request( let method = match res.master_password_hash { Some(_) => AuthRequestMethod::MasterKey { - protected_master_key: res.key.ok_or(Error::MissingFields)?.parse()?, - auth_request_key: r.key.ok_or(Error::MissingFields)?.parse()?, + protected_master_key: require!(res.key).parse()?, + auth_request_key: require!(r.key).parse()?, }, None => AuthRequestMethod::UserKey { - protected_user_key: res.key.ok_or(Error::MissingFields)?.parse()?, + protected_user_key: require!(res.key).parse()?, }, }; @@ -116,7 +112,7 @@ pub(crate) async fn complete_auth_request( .initialize_user_crypto(InitUserCryptoRequest { kdf_params: kdf, email: auth_req.email, - private_key: r.private_key.ok_or(Error::MissingFields)?, + private_key: require!(r.private_key), method: InitUserCryptoMethod::AuthRequest { request_private_key: auth_req.private_key, method, diff --git a/crates/bitwarden/src/auth/login/mod.rs b/crates/bitwarden/src/auth/login/mod.rs index 53ea712cc..745e0b78c 100644 --- a/crates/bitwarden/src/auth/login/mod.rs +++ b/crates/bitwarden/src/auth/login/mod.rs @@ -58,8 +58,7 @@ pub(crate) fn parse_prelogin(response: PreloginResponseModel) -> Result { use std::num::NonZeroU32; use bitwarden_api_identity::models::KdfType; - - use crate::util::{ + use bitwarden_crypto::{ default_argon2_iterations, default_argon2_memory, default_argon2_parallelism, default_pbkdf2_iterations, }; diff --git a/crates/bitwarden/src/auth/login/password.rs b/crates/bitwarden/src/auth/login/password.rs index 9fa62a566..db392025f 100644 --- a/crates/bitwarden/src/auth/login/password.rs +++ b/crates/bitwarden/src/auth/login/password.rs @@ -1,5 +1,7 @@ #[cfg(feature = "internal")] -use log::{debug, info}; +use bitwarden_crypto::SensitiveString; +#[cfg(feature = "internal")] +use log::info; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; @@ -20,22 +22,27 @@ use crate::{ #[cfg(feature = "internal")] pub(crate) async fn login_password( client: &mut Client, - input: &PasswordLoginRequest, + input: PasswordLoginRequest, ) -> Result { - use bitwarden_crypto::{EncString, HashPurpose}; + use bitwarden_crypto::{EncString, HashPurpose, MasterKey}; - use crate::{auth::determine_password_hash, client::UserLoginMethod, error::Error}; + use crate::{client::UserLoginMethod, error::require}; info!("password logging in"); - debug!("{:#?}, {:#?}", client, input); - let password_hash = determine_password_hash( + let password_vec = input.password.into(); + + let master_key = MasterKey::derive(&password_vec, input.email.as_bytes(), &input.kdf)?; + let password_hash = + master_key.derive_master_key_hash(&password_vec, HashPurpose::ServerAuthorization)?; + + let response = request_identity_tokens( + client, &input.email, - &input.kdf, - &input.password, - HashPurpose::ServerAuthorization, - )?; - let response = request_identity_tokens(client, input, &password_hash).await?; + &input.two_factor, + password_hash.expose(), + ) + .await?; if let IdentityTokenResponse::Authenticated(r) = &response { client.set_tokens( @@ -49,14 +56,10 @@ pub(crate) async fn login_password( kdf: input.kdf.to_owned(), })); - let user_key: EncString = r.key.as_deref().ok_or(Error::MissingFields)?.parse()?; - let private_key: EncString = r - .private_key - .as_deref() - .ok_or(Error::MissingFields)? - .parse()?; + let user_key: EncString = require!(r.key.as_deref()).parse()?; + let private_key: EncString = require!(r.private_key.as_deref()).parse()?; - client.initialize_user_crypto(&input.password, user_key, private_key)?; + client.initialize_user_crypto_master_key(master_key, user_key, private_key)?; } PasswordLoginResponse::process_response(response) @@ -65,18 +68,19 @@ pub(crate) async fn login_password( #[cfg(feature = "internal")] async fn request_identity_tokens( client: &mut Client, - input: &PasswordLoginRequest, + email: &str, + two_factor: &Option, password_hash: &str, ) -> Result { use crate::client::client_settings::DeviceType; let config = client.get_api_configurations().await; PasswordTokenRequest::new( - &input.email, + email, password_hash, DeviceType::ChromeBrowser, "b86dd6ab-4265-4ddf-a7f1-eb28d5677f33", - &input.two_factor, + two_factor, ) .send(config) .await @@ -90,7 +94,7 @@ pub struct PasswordLoginRequest { /// Bitwarden account email address pub email: String, /// Bitwarden account master password - pub password: String, + pub password: SensitiveString, // Two-factor authentication pub two_factor: Option, /// Kdf from prelogin diff --git a/crates/bitwarden/src/auth/login/two_factor.rs b/crates/bitwarden/src/auth/login/two_factor.rs index c8f0cc55b..d0533cd39 100644 --- a/crates/bitwarden/src/auth/login/two_factor.rs +++ b/crates/bitwarden/src/auth/login/two_factor.rs @@ -1,5 +1,5 @@ use bitwarden_api_api::models::TwoFactorEmailRequestModel; -use bitwarden_crypto::HashPurpose; +use bitwarden_crypto::{HashPurpose, SensitiveString}; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; use serde_repr::{Deserialize_repr, Serialize_repr}; @@ -9,15 +9,15 @@ use crate::{auth::determine_password_hash, error::Result, Client}; #[derive(Serialize, Deserialize, Debug, JsonSchema)] #[serde(rename_all = "camelCase", deny_unknown_fields)] pub struct TwoFactorEmailRequest { - /// User Password - pub password: String, /// User email pub email: String, + /// User Password + pub password: SensitiveString, } pub(crate) async fn send_two_factor_email( client: &mut Client, - input: &TwoFactorEmailRequest, + input: TwoFactorEmailRequest, ) -> Result<()> { // TODO: This should be resolved from the client let kdf = client.auth().prelogin(input.email.clone()).await?; @@ -25,7 +25,7 @@ pub(crate) async fn send_two_factor_email( let password_hash = determine_password_hash( &input.email, &kdf, - &input.password, + &input.password.into(), HashPurpose::ServerAuthorization, )?; @@ -33,7 +33,7 @@ pub(crate) async fn send_two_factor_email( bitwarden_api_api::apis::two_factor_api::two_factor_send_email_login_post( &config.api, Some(TwoFactorEmailRequestModel { - master_password_hash: Some(password_hash), + master_password_hash: Some(password_hash.expose().clone()), otp: None, auth_request_access_code: None, secret: None, diff --git a/crates/bitwarden/src/auth/mod.rs b/crates/bitwarden/src/auth/mod.rs index b1fe2dbda..48c56c6bd 100644 --- a/crates/bitwarden/src/auth/mod.rs +++ b/crates/bitwarden/src/auth/mod.rs @@ -11,7 +11,7 @@ pub use jwt_token::JWTToken; #[cfg(feature = "internal")] mod register; #[cfg(feature = "internal")] -use bitwarden_crypto::{HashPurpose, MasterKey}; +use bitwarden_crypto::{HashPurpose, MasterKey, SensitiveString, SensitiveVec}; #[cfg(feature = "internal")] pub use register::{RegisterKeyResponse, RegisterRequest}; #[cfg(feature = "internal")] @@ -32,11 +32,11 @@ use crate::{client::Kdf, error::Result}; fn determine_password_hash( email: &str, kdf: &Kdf, - password: &str, + password: &SensitiveVec, purpose: HashPurpose, -) -> Result { - let master_key = MasterKey::derive(password.as_bytes(), email.as_bytes(), kdf)?; - Ok(master_key.derive_master_key_hash(password.as_bytes(), purpose)?) +) -> Result { + let master_key = MasterKey::derive(password, email.as_bytes(), kdf)?; + Ok(master_key.derive_master_key_hash(password, purpose)?) } #[cfg(test)] @@ -50,14 +50,14 @@ mod tests { fn test_determine_password_hash() { use super::determine_password_hash; - let password = "password123"; + let password = SensitiveVec::test(b"password123"); let email = "test@bitwarden.com"; let kdf = Kdf::PBKDF2 { iterations: NonZeroU32::new(100_000).unwrap(), }; let purpose = HashPurpose::LocalAuthorization; - let result = determine_password_hash(email, &kdf, password, purpose).unwrap(); + let result = determine_password_hash(email, &kdf, &password, purpose).unwrap(); assert_eq!(result, "7kTqkF1pY/3JeOu73N9kR99fDDe9O1JOZaVc7KH3lsU="); } diff --git a/crates/bitwarden/src/auth/password/policy.rs b/crates/bitwarden/src/auth/password/policy.rs index dfd0e770d..78fee85e0 100644 --- a/crates/bitwarden/src/auth/password/policy.rs +++ b/crates/bitwarden/src/auth/password/policy.rs @@ -1,8 +1,9 @@ +use bitwarden_crypto::SensitiveString; use schemars::JsonSchema; /// Validate the provided password passes the provided Master Password Requirements Policy. pub(crate) fn satisfies_policy( - password: String, + password: SensitiveString, strength: u8, policy: &MasterPasswordPolicyOptions, ) -> bool { @@ -14,19 +15,19 @@ pub(crate) fn satisfies_policy( return false; } - if policy.require_upper && password.to_lowercase() == password { + if policy.require_upper && !password.any_chars(char::is_uppercase) { return false; } - if policy.require_lower && password.to_uppercase() == password { + if policy.require_lower && !password.any_chars(char::is_lowercase) { return false; } - if policy.require_numbers && !password.chars().any(|c| c.is_numeric()) { + if policy.require_numbers && !password.any_chars(char::is_numeric) { return false; } - if policy.require_special && !password.chars().any(|c| "!@#$%^&*".contains(c)) { + if policy.require_special && !password.any_chars(|c| "!@#$%^&*".contains(c)) { return false; } @@ -53,11 +54,13 @@ pub struct MasterPasswordPolicyOptions { #[cfg(test)] mod tests { + use bitwarden_crypto::SensitiveString; + use super::{satisfies_policy, MasterPasswordPolicyOptions}; #[test] fn satisfies_policy_gives_success() { - let password = "lkasfo!icbb$2323ALKJCO22".to_string(); + let password = SensitiveString::test("lkasfo!icbb$2323ALKJCO22"); let options = MasterPasswordPolicyOptions { min_complexity: 3, min_length: 5, @@ -74,7 +77,7 @@ mod tests { #[test] fn satisfies_policy_evaluates_strength() { - let password = "password123".to_string(); + let password = SensitiveString::test("password123"); let options = MasterPasswordPolicyOptions { min_complexity: 3, min_length: 0, @@ -91,7 +94,7 @@ mod tests { #[test] fn satisfies_policy_evaluates_length() { - let password = "password123".to_string(); + let password = SensitiveString::test("password123"); let options = MasterPasswordPolicyOptions { min_complexity: 0, min_length: 20, @@ -108,7 +111,7 @@ mod tests { #[test] fn satisfies_policy_evaluates_upper() { - let password = "password123".to_string(); + let password = SensitiveString::test("password123"); let options = MasterPasswordPolicyOptions { min_complexity: 0, min_length: 0, @@ -125,7 +128,7 @@ mod tests { #[test] fn satisfies_policy_evaluates_lower() { - let password = "ABCDEFG123".to_string(); + let password = SensitiveString::test("ABCDEFG123"); let options = MasterPasswordPolicyOptions { min_complexity: 0, min_length: 0, @@ -142,7 +145,7 @@ mod tests { #[test] fn satisfies_policy_evaluates_numbers() { - let password = "password".to_string(); + let password = SensitiveString::test("password"); let options = MasterPasswordPolicyOptions { min_complexity: 0, min_length: 0, @@ -159,7 +162,7 @@ mod tests { #[test] fn satisfies_policy_evaluates_special() { - let password = "Password123".to_string(); + let password = SensitiveString::test("Password123"); let options = MasterPasswordPolicyOptions { min_complexity: 0, min_length: 0, diff --git a/crates/bitwarden/src/auth/password/strength.rs b/crates/bitwarden/src/auth/password/strength.rs index 66b5b5823..d958ae7c2 100644 --- a/crates/bitwarden/src/auth/password/strength.rs +++ b/crates/bitwarden/src/auth/password/strength.rs @@ -1,9 +1,10 @@ +use bitwarden_crypto::SensitiveString; use zxcvbn::zxcvbn; const GLOBAL_INPUTS: [&str; 3] = ["bitwarden", "bit", "warden"]; pub(crate) fn password_strength( - password: String, + password: SensitiveString, email: String, additional_inputs: Vec, ) -> u8 { @@ -13,7 +14,7 @@ pub(crate) fn password_strength( let mut arr: Vec<_> = inputs.iter().map(String::as_str).collect(); arr.extend(GLOBAL_INPUTS); - zxcvbn(&password, &arr).map_or(0, |e| e.score()) + zxcvbn(password.expose(), &arr).map_or(0, |e| e.score()) } fn email_to_user_inputs(email: &str) -> Vec { @@ -31,6 +32,8 @@ fn email_to_user_inputs(email: &str) -> Vec { #[cfg(test)] mod tests { + use bitwarden_crypto::SensitiveString; + use super::{email_to_user_inputs, password_strength}; #[test] @@ -44,7 +47,8 @@ mod tests { ]; for (password, email, expected) in cases { - let result = password_strength(password.to_owned(), email.to_owned(), vec![]); + let result = + password_strength(SensitiveString::test(password), email.to_owned(), vec![]); assert_eq!(expected, result, "{email}: {password}"); } } @@ -54,14 +58,14 @@ mod tests { let password = "asdfjkhkjwer!"; let result = password_strength( - password.to_owned(), + SensitiveString::test(password), "random@bitwarden.com".to_owned(), vec![], ); assert_eq!(result, 4); let result = password_strength( - password.to_owned(), + SensitiveString::test(password), "asdfjkhkjwer@bitwarden.com".to_owned(), vec![], ); diff --git a/crates/bitwarden/src/auth/password/validate.rs b/crates/bitwarden/src/auth/password/validate.rs index 9003347d9..f096dfd77 100644 --- a/crates/bitwarden/src/auth/password/validate.rs +++ b/crates/bitwarden/src/auth/password/validate.rs @@ -1,4 +1,4 @@ -use bitwarden_crypto::{HashPurpose, MasterKey}; +use bitwarden_crypto::{HashPurpose, MasterKey, SensitiveString}; use crate::{ auth::determine_password_hash, @@ -10,8 +10,8 @@ use crate::{ /// Validate if the provided password matches the password hash stored in the client. pub(crate) fn validate_password( client: &Client, - password: String, - password_hash: String, + password: SensitiveString, + password_hash: SensitiveString, ) -> Result { let login_method = client .login_method @@ -25,7 +25,7 @@ pub(crate) fn validate_password( let hash = determine_password_hash( email, kdf, - &password, + &password.into(), HashPurpose::LocalAuthorization, )?; @@ -40,9 +40,9 @@ pub(crate) fn validate_password( #[cfg(feature = "internal")] pub(crate) fn validate_password_user_key( client: &Client, - password: String, + password: SensitiveString, encrypted_user_key: String, -) -> Result { +) -> Result { let login_method = client .login_method .as_ref() @@ -52,7 +52,8 @@ pub(crate) fn validate_password_user_key( match login_method { UserLoginMethod::Username { email, kdf, .. } | UserLoginMethod::ApiKey { email, kdf, .. } => { - let master_key = MasterKey::derive(password.as_bytes(), email.as_bytes(), kdf)?; + let password_vec = password.into(); + let master_key = MasterKey::derive(&password_vec, email.as_bytes(), kdf)?; let user_key = master_key .decrypt_user_key(encrypted_user_key.parse()?) .map_err(|_| "wrong password")?; @@ -68,7 +69,7 @@ pub(crate) fn validate_password_user_key( } Ok(master_key - .derive_master_key_hash(password.as_bytes(), HashPurpose::LocalAuthorization)?) + .derive_master_key_hash(&password_vec, HashPurpose::LocalAuthorization)?) } } } else { @@ -78,6 +79,8 @@ pub(crate) fn validate_password_user_key( #[cfg(test)] mod tests { + use bitwarden_crypto::SensitiveString; + use crate::auth::password::{validate::validate_password_user_key, validate_password}; #[test] @@ -95,8 +98,8 @@ mod tests { client_id: "1".to_string(), })); - let password = "password123".to_string(); - let password_hash = "7kTqkF1pY/3JeOu73N9kR99fDDe9O1JOZaVc7KH3lsU=".to_string(); + let password = SensitiveString::test("password123"); + let password_hash = SensitiveString::test("7kTqkF1pY/3JeOu73N9kR99fDDe9O1JOZaVc7KH3lsU="); let result = validate_password(&client, password, password_hash); @@ -108,30 +111,43 @@ mod tests { fn test_validate_password_user_key() { use std::num::NonZeroU32; + use bitwarden_crypto::SensitiveVec; + use crate::client::{Client, Kdf, LoginMethod, UserLoginMethod}; let mut client = Client::new(None); + + let password = SensitiveVec::test(b"asdfasdfasdf"); + let email = "test@bitwarden.com"; + let kdf = Kdf::PBKDF2 { + iterations: NonZeroU32::new(600_000).unwrap(), + }; + client.set_login_method(LoginMethod::User(UserLoginMethod::Username { - email: "test@bitwarden.com".to_string(), - kdf: Kdf::PBKDF2 { - iterations: NonZeroU32::new(600_000).unwrap(), - }, + email: email.to_string(), + kdf: kdf.clone(), client_id: "1".to_string(), })); + let master_key = + bitwarden_crypto::MasterKey::derive(&password, email.as_bytes(), &kdf).unwrap(); + let user_key = "2.Q/2PhzcC7GdeiMHhWguYAQ==|GpqzVdr0go0ug5cZh1n+uixeBC3oC90CIe0hd/HWA/pTRDZ8ane4fmsEIcuc8eMKUt55Y2q/fbNzsYu41YTZzzsJUSeqVjT8/iTQtgnNdpo=|dwI+uyvZ1h/iZ03VQ+/wrGEFYVewBUUl/syYgjsNMbE="; 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.parse().unwrap(), private_key) + .initialize_user_crypto_master_key(master_key, user_key.parse().unwrap(), private_key) .unwrap(); - let result = - validate_password_user_key(&client, "asdfasdfasdf".to_string(), user_key.to_string()) - .unwrap(); + let result = validate_password_user_key( + &client, + SensitiveString::test("asdfasdfasdf"), + user_key.to_string(), + ) + .unwrap(); assert_eq!(result, "aOvkBXFhSdgrBWR3hZCMRoML9+h5yRblU3lFphCdkeA="); - assert!(validate_password(&client, "asdfasdfasdf".to_string(), result.to_string()).unwrap()) + assert!(validate_password(&client, password.try_into().unwrap(), result).unwrap()) } #[cfg(feature = "internal")] @@ -139,26 +155,37 @@ mod tests { fn test_validate_password_user_key_wrong_password() { use std::num::NonZeroU32; + use bitwarden_crypto::SensitiveVec; + use crate::client::{Client, Kdf, LoginMethod, UserLoginMethod}; let mut client = Client::new(None); + + let password = SensitiveVec::test(b"asdfasdfasdf"); + let email = "test@bitwarden.com"; + let kdf = Kdf::PBKDF2 { + iterations: NonZeroU32::new(600_000).unwrap(), + }; + client.set_login_method(LoginMethod::User(UserLoginMethod::Username { - email: "test@bitwarden.com".to_string(), - kdf: Kdf::PBKDF2 { - iterations: NonZeroU32::new(600_000).unwrap(), - }, + email: email.to_string(), + kdf: kdf.clone(), client_id: "1".to_string(), })); + let master_key = + bitwarden_crypto::MasterKey::derive(&password, email.as_bytes(), &kdf).unwrap(); + let user_key = "2.Q/2PhzcC7GdeiMHhWguYAQ==|GpqzVdr0go0ug5cZh1n+uixeBC3oC90CIe0hd/HWA/pTRDZ8ane4fmsEIcuc8eMKUt55Y2q/fbNzsYu41YTZzzsJUSeqVjT8/iTQtgnNdpo=|dwI+uyvZ1h/iZ03VQ+/wrGEFYVewBUUl/syYgjsNMbE="; 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.parse().unwrap(), private_key) + .initialize_user_crypto_master_key(master_key, user_key.parse().unwrap(), private_key) .unwrap(); - let result = validate_password_user_key(&client, "abc".to_string(), user_key.to_string()) - .unwrap_err(); + let result = + validate_password_user_key(&client, SensitiveString::test("abc"), user_key.to_string()) + .unwrap_err(); assert_eq!(result.to_string(), "Internal error: wrong password"); } diff --git a/crates/bitwarden/src/auth/register.rs b/crates/bitwarden/src/auth/register.rs index 31b69c515..50e4e41cb 100644 --- a/crates/bitwarden/src/auth/register.rs +++ b/crates/bitwarden/src/auth/register.rs @@ -2,40 +2,40 @@ use bitwarden_api_identity::{ apis::accounts_api::accounts_register_post, models::{KeysRequestModel, RegisterRequestModel}, }; -use bitwarden_crypto::{HashPurpose, MasterKey, RsaKeyPair}; +use bitwarden_crypto::{ + default_pbkdf2_iterations, HashPurpose, MasterKey, RsaKeyPair, SensitiveString, +}; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; -use crate::{client::Kdf, error::Result, util::default_pbkdf2_iterations, Client}; +use crate::{client::Kdf, error::Result, Client}; #[derive(Serialize, Deserialize, Debug, JsonSchema)] #[serde(rename_all = "camelCase", deny_unknown_fields)] pub struct RegisterRequest { pub email: String, pub name: Option, - pub password: String, + pub password: SensitiveString, pub password_hint: Option, } /// Half baked implementation of user registration -pub(super) async fn register(client: &mut Client, req: &RegisterRequest) -> Result<()> { +pub(super) async fn register(client: &mut Client, req: RegisterRequest) -> Result<()> { let config = client.get_api_configurations().await; - let kdf = Kdf::PBKDF2 { - iterations: default_pbkdf2_iterations(), - }; + let kdf = Kdf::default(); - let keys = make_register_keys(req.email.to_owned(), req.password.to_owned(), kdf)?; + let keys = make_register_keys(req.email.clone(), req.password, kdf)?; accounts_register_post( &config.identity, Some(RegisterRequestModel { - name: req.name.to_owned(), - email: req.email.to_owned(), - master_password_hash: keys.master_password_hash, - master_password_hint: req.password_hint.to_owned(), + name: req.name, + email: req.email, + master_password_hash: keys.master_password_hash.expose().clone(), + master_password_hint: req.password_hint, captcha_response: None, // TODO: Add - key: Some(keys.encrypted_user_key.to_string()), + key: Some(keys.encrypted_user_key), keys: Some(Box::new(KeysRequestModel { public_key: Some(keys.keys.public), encrypted_private_key: keys.keys.private.to_string(), @@ -56,12 +56,13 @@ pub(super) async fn register(client: &mut Client, req: &RegisterRequest) -> Resu pub(super) fn make_register_keys( email: String, - password: String, + password: SensitiveString, kdf: Kdf, ) -> Result { - let master_key = MasterKey::derive(password.as_bytes(), email.as_bytes(), &kdf)?; + let password_vec = password.into(); + let master_key = MasterKey::derive(&password_vec, email.as_bytes(), &kdf)?; let master_password_hash = - master_key.derive_master_key_hash(password.as_bytes(), HashPurpose::ServerAuthorization)?; + master_key.derive_master_key_hash(&password_vec, HashPurpose::ServerAuthorization)?; let (user_key, encrypted_user_key) = master_key.make_user_key()?; let keys = user_key.make_key_pair()?; @@ -74,7 +75,7 @@ pub(super) fn make_register_keys( #[cfg_attr(feature = "mobile", derive(uniffi::Record))] pub struct RegisterKeyResponse { - pub master_password_hash: String, + pub master_password_hash: SensitiveString, pub encrypted_user_key: String, pub keys: RsaKeyPair, } diff --git a/crates/bitwarden/src/auth/renew.rs b/crates/bitwarden/src/auth/renew.rs index 6f93f7482..783494ae2 100644 --- a/crates/bitwarden/src/auth/renew.rs +++ b/crates/bitwarden/src/auth/renew.rs @@ -56,11 +56,8 @@ pub(crate) async fn renew_token(client: &mut Client) -> Result<()> { .send(&client.__api_configurations) .await?; - if let ( - IdentityTokenResponse::Authenticated(r), - Some(state_file), - Ok(enc_settings), - ) = (&result, state_file, client.get_encryption_settings()) + if let (IdentityTokenResponse::Payload(r), Some(state_file), Ok(enc_settings)) = + (&result, state_file, client.get_encryption_settings()) { if let Some(enc_key) = enc_settings.get_key(&None) { let state = @@ -83,6 +80,10 @@ pub(crate) async fn renew_token(client: &mut Client) -> Result<()> { client.set_tokens(r.access_token, r.refresh_token, r.expires_in); return Ok(()); } + IdentityTokenResponse::Payload(r) => { + client.set_tokens(r.access_token, r.refresh_token, r.expires_in); + return Ok(()); + } _ => { // We should never get here return Err(Error::InvalidResponse); diff --git a/crates/bitwarden/src/auth/tde.rs b/crates/bitwarden/src/auth/tde.rs index 1a3de3026..94f70de97 100644 --- a/crates/bitwarden/src/auth/tde.rs +++ b/crates/bitwarden/src/auth/tde.rs @@ -1,6 +1,6 @@ use base64::{engine::general_purpose::STANDARD, Engine}; use bitwarden_crypto::{ - AsymmetricEncString, AsymmetricPublicCryptoKey, DeviceKey, EncString, SymmetricCryptoKey, + AsymmetricEncString, AsymmetricPublicCryptoKey, DeviceKey, EncString, Kdf, SymmetricCryptoKey, TrustDeviceResponse, UserKey, }; @@ -11,6 +11,7 @@ use crate::{error::Result, Client}; /// password reset. If remember_device is true, it also generates a device key. pub(super) fn make_register_tde_keys( client: &mut Client, + email: String, org_public_key: String, remember_device: bool, ) -> Result { @@ -22,7 +23,7 @@ pub(super) fn make_register_tde_keys( let key_pair = user_key.make_key_pair()?; let admin_reset = - AsymmetricEncString::encrypt_rsa2048_oaep_sha1(&user_key.0.to_vec(), &public_key)?; + AsymmetricEncString::encrypt_rsa2048_oaep_sha1(user_key.0.to_vec(), &public_key)?; let device_key = if remember_device { Some(DeviceKey::trust_device(&user_key.0)?) @@ -30,6 +31,13 @@ pub(super) fn make_register_tde_keys( None }; + client.set_login_method(crate::client::LoginMethod::User( + crate::client::UserLoginMethod::Username { + client_id: "".to_owned(), + email, + kdf: Kdf::default(), + }, + )); client.initialize_user_crypto_decrypted_key(user_key.0, key_pair.private.clone())?; Ok(RegisterTdeKeyResponse { diff --git a/crates/bitwarden/src/client/client.rs b/crates/bitwarden/src/client/client.rs index 68442d5d3..95a1e8738 100644 --- a/crates/bitwarden/src/client/client.rs +++ b/crates/bitwarden/src/client/client.rs @@ -4,7 +4,7 @@ use std::path::PathBuf; pub use bitwarden_crypto::Kdf; use bitwarden_crypto::SymmetricCryptoKey; #[cfg(feature = "internal")] -use bitwarden_crypto::{AsymmetricEncString, EncString}; +use bitwarden_crypto::{AsymmetricEncString, EncString, MasterKey}; use chrono::Utc; use reqwest::header::{self, HeaderValue}; use uuid::Uuid; @@ -73,6 +73,7 @@ pub(crate) enum ServiceAccountLoginMethod { }, } +/// The main struct to interact with the Bitwarden SDK. #[derive(Debug)] pub struct Client { token: Option, @@ -108,16 +109,17 @@ impl Client { client_builder } - let external_client = new_client_builder().build().unwrap(); + let external_client = new_client_builder().build().expect("Build should not fail"); let mut headers = header::HeaderMap::new(); headers.append( "Device-Type", - HeaderValue::from_str(&(settings.device_type as u8).to_string()).unwrap(), + HeaderValue::from_str(&(settings.device_type as u8).to_string()) + .expect("All numbers are valid ASCII"), ); let client_builder = new_client_builder().default_headers(headers); - let client = client_builder.build().unwrap(); + let client = client_builder.build().expect("Build should not fail"); let identity = bitwarden_api_identity::apis::configuration::Configuration { base_path: settings.identity_url, @@ -195,7 +197,7 @@ impl Client { #[cfg(feature = "internal")] pub async fn get_user_api_key( &mut self, - input: &SecretVerificationRequest, + input: SecretVerificationRequest, ) -> Result { get_user_api_key(self, input).await } @@ -245,24 +247,17 @@ impl Client { } #[cfg(feature = "internal")] - pub(crate) fn initialize_user_crypto( + pub(crate) fn initialize_user_crypto_master_key( &mut self, - password: &str, + master_key: MasterKey, user_key: EncString, private_key: EncString, ) -> Result<&EncryptionSettings> { - let login_method = match &self.login_method { - Some(LoginMethod::User(u)) => u, - _ => return Err(Error::NotAuthenticated), - }; - - self.encryption_settings = Some(EncryptionSettings::new( - login_method, - password, + Ok(self.encryption_settings.insert(EncryptionSettings::new( + master_key, user_key, private_key, - )?); - Ok(self.encryption_settings.as_ref().unwrap()) + )?)) } #[cfg(feature = "internal")] @@ -271,33 +266,21 @@ impl Client { user_key: SymmetricCryptoKey, private_key: EncString, ) -> Result<&EncryptionSettings> { - self.encryption_settings = Some(EncryptionSettings::new_decrypted_key( - user_key, - private_key, - )?); Ok(self .encryption_settings - .as_ref() - .expect("It was initialized on the previous line")) + .insert(EncryptionSettings::new_decrypted_key( + user_key, + private_key, + )?)) } #[cfg(feature = "mobile")] pub(crate) fn initialize_user_crypto_pin( &mut self, - pin: &str, + pin_key: MasterKey, pin_protected_user_key: EncString, private_key: EncString, ) -> Result<&EncryptionSettings> { - use bitwarden_crypto::MasterKey; - - let pin_key = match &self.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 decrypted_user_key = pin_key.decrypt_user_key(pin_protected_user_key)?; self.initialize_user_crypto_decrypted_key(decrypted_user_key, private_key) } @@ -306,8 +289,8 @@ impl Client { &mut self, key: SymmetricCryptoKey, ) -> &EncryptionSettings { - self.encryption_settings = Some(EncryptionSettings::new_single_key(key)); - self.encryption_settings.as_ref().unwrap() + self.encryption_settings + .insert(EncryptionSettings::new_single_key(key)) } #[cfg(feature = "internal")] @@ -321,7 +304,7 @@ impl Client { .ok_or(Error::VaultLocked)?; enc.set_org_keys(org_keys)?; - Ok(self.encryption_settings.as_ref().unwrap()) + Ok(&*enc) } } diff --git a/crates/bitwarden/src/client/encryption_settings.rs b/crates/bitwarden/src/client/encryption_settings.rs index 6e4da9895..463c67815 100644 --- a/crates/bitwarden/src/client/encryption_settings.rs +++ b/crates/bitwarden/src/client/encryption_settings.rs @@ -2,11 +2,11 @@ use std::collections::HashMap; use bitwarden_crypto::{AsymmetricCryptoKey, KeyContainer, SymmetricCryptoKey}; #[cfg(feature = "internal")] -use bitwarden_crypto::{AsymmetricEncString, EncString}; +use bitwarden_crypto::{AsymmetricEncString, EncString, MasterKey}; use uuid::Uuid; #[cfg(feature = "internal")] -use crate::{client::UserLoginMethod, error::Result}; +use crate::error::Result; pub struct EncryptionSettings { user_key: SymmetricCryptoKey, @@ -21,28 +21,16 @@ impl std::fmt::Debug for EncryptionSettings { } impl EncryptionSettings { - /// Initialize the encryption settings with the user password and their encrypted keys + /// Initialize the encryption settings with the master key and the encrypted user keys #[cfg(feature = "internal")] pub(crate) fn new( - login_method: &UserLoginMethod, - password: &str, + master_key: MasterKey, user_key: EncString, private_key: EncString, ) -> Result { - use bitwarden_crypto::MasterKey; - - match login_method { - UserLoginMethod::Username { email, kdf, .. } - | UserLoginMethod::ApiKey { email, kdf, .. } => { - // Derive master key from password - let master_key = MasterKey::derive(password.as_bytes(), email.as_bytes(), kdf)?; - - // Decrypt the user key - let user_key = master_key.decrypt_user_key(user_key)?; - - Self::new_decrypted_key(user_key, private_key) - } - } + // Decrypt the user key + let user_key = master_key.decrypt_user_key(user_key)?; + Self::new_decrypted_key(user_key, private_key) } /// Initialize the encryption settings with the decrypted user key and the encrypted user @@ -54,11 +42,11 @@ impl EncryptionSettings { user_key: SymmetricCryptoKey, private_key: EncString, ) -> Result { - use bitwarden_crypto::KeyDecryptable; + use bitwarden_crypto::{DecryptedVec, KeyDecryptable}; let private_key = { - let dec: Vec = private_key.decrypt_with_key(&user_key)?; - Some(AsymmetricCryptoKey::from_der(&dec)?) + let dec: DecryptedVec = private_key.decrypt_with_key(&user_key)?; + Some(AsymmetricCryptoKey::from_der(dec)?) }; Ok(EncryptionSettings { @@ -83,7 +71,7 @@ impl EncryptionSettings { &mut self, org_enc_keys: Vec<(Uuid, AsymmetricEncString)>, ) -> Result<&mut Self> { - use bitwarden_crypto::KeyDecryptable; + use bitwarden_crypto::{DecryptedVec, KeyDecryptable}; use crate::error::Error; @@ -95,9 +83,9 @@ impl EncryptionSettings { // Decrypt the org keys with the private key for (org_id, org_enc_key) in org_enc_keys { - let mut dec: Vec = org_enc_key.decrypt_with_key(private_key)?; + let dec: DecryptedVec = org_enc_key.decrypt_with_key(private_key)?; - let org_key = SymmetricCryptoKey::try_from(dec.as_mut_slice())?; + let org_key = SymmetricCryptoKey::try_from(dec)?; self.org_keys.insert(org_id, org_key); } diff --git a/crates/bitwarden/src/error.rs b/crates/bitwarden/src/error.rs index bd040e51b..fdae93fac 100644 --- a/crates/bitwarden/src/error.rs +++ b/crates/bitwarden/src/error.rs @@ -8,6 +8,7 @@ use bitwarden_api_identity::apis::Error as IdentityError; use bitwarden_exporters::ExportError; #[cfg(feature = "internal")] use bitwarden_generators::{PassphraseError, PasswordError, UsernameError}; +use passkey::client::WebauthnError; use reqwest::StatusCode; use thiserror::Error; @@ -24,8 +25,8 @@ pub enum Error { #[error("The response received was invalid and could not be processed")] InvalidResponse, - #[error("The response received was missing some of the required fields")] - MissingFields, + #[error("The response received was missing some of the required fields: {0}")] + MissingFields(&'static str), #[error("Cryptography error, {0}")] Crypto(#[from] bitwarden_crypto::CryptoError), @@ -68,10 +69,23 @@ pub enum Error { #[error(transparent)] ExportError(#[from] ExportError), + #[error("Webauthn error: {0:?}")] + WebauthnError(passkey::client::WebauthnError), + + #[cfg(feature = "mobile")] + #[error("Uniffi callback error: {0}")] + UniffiCallback(#[from] uniffi::UnexpectedUniFFICallbackError), + #[error("Internal error: {0}")] Internal(Cow<'static, str>), } +impl From for Error { + fn from(e: WebauthnError) -> Self { + Self::WebauthnError(e) + } +} + impl From for Error { fn from(s: String) -> Self { Self::Internal(s.into()) @@ -133,4 +147,18 @@ macro_rules! impl_bitwarden_error { impl_bitwarden_error!(ApiError); impl_bitwarden_error!(IdentityError); +/// This macro is used to require that a value is present or return an error otherwise. +/// It is equivalent to using `val.ok_or(Error::MissingFields)?`, but easier to use and +/// with a more descriptive error message. +/// Note that this macro will return early from the function if the value is not present. +macro_rules! require { + ($val:expr) => { + match $val { + Some(val) => val, + None => return Err($crate::error::Error::MissingFields(stringify!($val))), + } + }; +} +pub(crate) use require; + pub type Result = std::result::Result; diff --git a/crates/bitwarden/src/mobile/client_crypto.rs b/crates/bitwarden/src/mobile/client_crypto.rs index 6ef65975d..6259e084d 100644 --- a/crates/bitwarden/src/mobile/client_crypto.rs +++ b/crates/bitwarden/src/mobile/client_crypto.rs @@ -1,5 +1,5 @@ #[cfg(feature = "internal")] -use bitwarden_crypto::{AsymmetricEncString, EncString}; +use bitwarden_crypto::{AsymmetricEncString, EncString, SensitiveString}; use crate::Client; #[cfg(feature = "internal")] @@ -28,20 +28,20 @@ impl<'a> ClientCrypto<'a> { } #[cfg(feature = "internal")] - pub async fn get_user_encryption_key(&mut self) -> Result { + pub async fn get_user_encryption_key(&mut self) -> Result { get_user_encryption_key(self.client).await } #[cfg(feature = "internal")] pub async fn update_password( &mut self, - new_password: String, + new_password: SensitiveString, ) -> Result { update_password(self.client, new_password) } #[cfg(feature = "internal")] - pub async fn derive_pin_key(&mut self, pin: String) -> Result { + pub async fn derive_pin_key(&mut self, pin: SensitiveString) -> Result { derive_pin_key(self.client, pin) } diff --git a/crates/bitwarden/src/mobile/client_kdf.rs b/crates/bitwarden/src/mobile/client_kdf.rs index 4e62e5d59..2a5c792bd 100644 --- a/crates/bitwarden/src/mobile/client_kdf.rs +++ b/crates/bitwarden/src/mobile/client_kdf.rs @@ -1,4 +1,4 @@ -use bitwarden_crypto::HashPurpose; +use bitwarden_crypto::{HashPurpose, SensitiveString}; use crate::{client::Kdf, error::Result, mobile::kdf::hash_password, Client}; @@ -10,10 +10,10 @@ impl<'a> ClientKdf<'a> { pub async fn hash_password( &self, email: String, - password: String, + password: SensitiveString, kdf_params: Kdf, purpose: HashPurpose, - ) -> Result { + ) -> Result { hash_password(self.client, email, password, kdf_params, purpose).await } } diff --git a/crates/bitwarden/src/mobile/crypto.rs b/crates/bitwarden/src/mobile/crypto.rs index 214643023..62006503b 100644 --- a/crates/bitwarden/src/mobile/crypto.rs +++ b/crates/bitwarden/src/mobile/crypto.rs @@ -2,7 +2,9 @@ use std::collections::HashMap; use bitwarden_crypto::{AsymmetricEncString, EncString}; #[cfg(feature = "internal")] -use bitwarden_crypto::{KeyDecryptable, KeyEncryptable, MasterKey, SymmetricCryptoKey}; +use bitwarden_crypto::{ + KeyDecryptable, KeyEncryptable, MasterKey, SensitiveString, SymmetricCryptoKey, +}; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; @@ -36,30 +38,30 @@ pub struct InitUserCryptoRequest { pub enum InitUserCryptoMethod { Password { /// The user's master password - password: String, + password: SensitiveString, /// The user's encrypted symmetric crypto key user_key: String, }, DecryptedKey { /// The user's decrypted encryption key, obtained using `get_user_encryption_key` - decrypted_user_key: String, + decrypted_user_key: SensitiveString, }, Pin { /// The user's PIN - pin: String, + pin: SensitiveString, /// 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, + request_private_key: SensitiveString, method: AuthRequestMethod, }, DeviceKey { /// The device's DeviceKey - device_key: String, + device_key: SensitiveString, /// The Device Private Key protected_device_private_key: EncString, /// The user's symmetric crypto key, encrypted with the Device Key. @@ -90,29 +92,26 @@ pub async fn initialize_user_crypto(client: &mut Client, req: InitUserCryptoRequ use crate::auth::{auth_request_decrypt_master_key, auth_request_decrypt_user_key}; - let login_method = crate::client::LoginMethod::User(crate::client::UserLoginMethod::Username { - client_id: "".to_string(), - email: req.email, - kdf: req.kdf_params, - }); - client.set_login_method(login_method); - let private_key: EncString = req.private_key.parse()?; match req.method { InitUserCryptoMethod::Password { password, user_key } => { let user_key: EncString = user_key.parse()?; - client.initialize_user_crypto(&password, user_key, private_key)?; + + let master_key = + MasterKey::derive(&password.into(), req.email.as_bytes(), &req.kdf_params)?; + client.initialize_user_crypto_master_key(master_key, user_key, private_key)?; } InitUserCryptoMethod::DecryptedKey { decrypted_user_key } => { - let user_key = decrypted_user_key.parse::()?; + let user_key = SymmetricCryptoKey::try_from(decrypted_user_key)?; client.initialize_user_crypto_decrypted_key(user_key, private_key)?; } InitUserCryptoMethod::Pin { pin, pin_protected_user_key, } => { - client.initialize_user_crypto_pin(&pin, pin_protected_user_key, private_key)?; + let pin_key = MasterKey::derive(&pin.into(), req.email.as_bytes(), &req.kdf_params)?; + client.initialize_user_crypto_pin(pin_key, pin_protected_user_key, private_key)?; } InitUserCryptoMethod::AuthRequest { request_private_key, @@ -138,7 +137,7 @@ pub async fn initialize_user_crypto(client: &mut Client, req: InitUserCryptoRequ protected_device_private_key, device_protected_user_key, } => { - let device_key = device_key.parse::()?; + let device_key = DeviceKey::try_from(device_key)?; let user_key = device_key .decrypt_user_key(protected_device_private_key, device_protected_user_key)?; @@ -146,6 +145,14 @@ pub async fn initialize_user_crypto(client: &mut Client, req: InitUserCryptoRequ } } + client.set_login_method(crate::client::LoginMethod::User( + crate::client::UserLoginMethod::Username { + client_id: "".to_string(), + email: req.email, + kdf: req.kdf_params, + }, + )); + Ok(()) } @@ -166,7 +173,7 @@ pub async fn initialize_org_crypto(client: &mut Client, req: InitOrgCryptoReques } #[cfg(feature = "internal")] -pub async fn get_user_encryption_key(client: &mut Client) -> Result { +pub async fn get_user_encryption_key(client: &mut Client) -> Result { let user_key = client .get_encryption_settings()? .get_key(&None) @@ -181,14 +188,14 @@ pub async fn get_user_encryption_key(client: &mut Client) -> Result { #[cfg_attr(feature = "mobile", derive(uniffi::Record))] pub struct UpdatePasswordResponse { /// Hash of the new password - password_hash: String, + password_hash: SensitiveString, /// User key, encrypted with the new password new_key: EncString, } pub fn update_password( client: &mut Client, - new_password: String, + new_password: SensitiveString, ) -> Result { let user_key = client .get_encryption_settings()? @@ -200,19 +207,21 @@ pub fn update_password( .as_ref() .ok_or(Error::NotAuthenticated)?; + let password_vec = new_password.into(); + // Derive a new master key from password let new_master_key = match login_method { LoginMethod::User( UserLoginMethod::Username { email, kdf, .. } | UserLoginMethod::ApiKey { email, kdf, .. }, - ) => MasterKey::derive(new_password.as_bytes(), email.as_bytes(), kdf)?, + ) => MasterKey::derive(&password_vec, email.as_bytes(), kdf)?, _ => return Err(Error::NotAuthenticated), }; let new_key = new_master_key.encrypt_user_key(user_key)?; let password_hash = new_master_key.derive_master_key_hash( - new_password.as_bytes(), + &password_vec, bitwarden_crypto::HashPurpose::ServerAuthorization, )?; @@ -227,14 +236,14 @@ pub fn update_password( #[serde(rename_all = "camelCase", deny_unknown_fields)] #[cfg_attr(feature = "mobile", derive(uniffi::Record))] pub struct DerivePinKeyResponse { - /// [UserKey] protected by PIN + /// [UserKey](bitwarden_crypto::UserKey) protected by PIN pin_protected_user_key: EncString, - /// PIN protected by [UserKey] + /// PIN protected by [UserKey](bitwarden_crypto::UserKey) encrypted_pin: EncString, } #[cfg(feature = "internal")] -pub fn derive_pin_key(client: &mut Client, pin: String) -> Result { +pub fn derive_pin_key(client: &mut Client, pin: SensitiveString) -> Result { let user_key = client .get_encryption_settings()? .get_key(&None) @@ -245,7 +254,8 @@ pub fn derive_pin_key(client: &mut Client, pin: String) -> Result Result Result { + use bitwarden_crypto::DecryptedString; + 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 pin: DecryptedString = 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) + derive_pin_protected_user_key(pin, login_method, user_key) } #[cfg(feature = "internal")] fn derive_pin_protected_user_key( - pin: &str, + pin: SensitiveString, login_method: &LoginMethod, user_key: &SymmetricCryptoKey, ) -> Result { @@ -279,7 +292,7 @@ fn derive_pin_protected_user_key( LoginMethod::User( UserLoginMethod::Username { email, kdf, .. } | UserLoginMethod::ApiKey { email, kdf, .. }, - ) => MasterKey::derive(pin.as_bytes(), email.as_bytes(), kdf)?, + ) => MasterKey::derive(&pin.into(), email.as_bytes(), kdf)?, _ => return Err(Error::NotAuthenticated), }; @@ -294,12 +307,12 @@ pub(super) fn enroll_admin_password_reset( use base64::{engine::general_purpose::STANDARD, Engine}; use bitwarden_crypto::AsymmetricPublicCryptoKey; - let public_key = AsymmetricPublicCryptoKey::from_der(&STANDARD.decode(public_key)?)?; + let public_key = AsymmetricPublicCryptoKey::from_der(STANDARD.decode(public_key)?.as_slice())?; 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(), + key.to_vec(), &public_key, )?) } @@ -326,7 +339,7 @@ mod tests { email: "test@bitwarden.com".into(), private_key: priv_key.to_owned(), method: InitUserCryptoMethod::Password { - password: "asdfasdfasdf".into(), + password: SensitiveString::test("asdfasdfasdf"), user_key: "2.u2HDQ/nH2J7f5tYHctZx6Q==|NnUKODz8TPycWJA5svexe1wJIz2VexvLbZh2RDfhj5VI3wP8ZkR0Vicvdv7oJRyLI1GyaZDBCf9CTBunRTYUk39DbZl42Rb+Xmzds02EQhc=|rwuo5wgqvTJf3rgwOUfabUyzqhguMYb3sGBjOYqjevc=".into(), }, }, @@ -334,7 +347,8 @@ mod tests { .await .unwrap(); - let new_password_response = update_password(&mut client, "123412341234".into()).unwrap(); + let new_password_response = + update_password(&mut client, SensitiveString::test("123412341234")).unwrap(); let mut client2 = Client::new(None); @@ -345,7 +359,7 @@ mod tests { email: "test@bitwarden.com".into(), private_key: priv_key.to_owned(), method: InitUserCryptoMethod::Password { - password: "123412341234".into(), + password: SensitiveString::test("123412341234"), user_key: new_password_response.new_key.to_string(), }, }, @@ -357,7 +371,7 @@ mod tests { .kdf() .hash_password( "test@bitwarden.com".into(), - "123412341234".into(), + SensitiveString::test("123412341234"), kdf.clone(), bitwarden_crypto::HashPurpose::ServerAuthorization, ) @@ -397,7 +411,7 @@ mod tests { email: "test@bitwarden.com".into(), private_key: priv_key.to_owned(), method: InitUserCryptoMethod::Password { - password: "asdfasdfasdf".into(), + password: SensitiveString::test("asdfasdfasdf"), user_key: "2.u2HDQ/nH2J7f5tYHctZx6Q==|NnUKODz8TPycWJA5svexe1wJIz2VexvLbZh2RDfhj5VI3wP8ZkR0Vicvdv7oJRyLI1GyaZDBCf9CTBunRTYUk39DbZl42Rb+Xmzds02EQhc=|rwuo5wgqvTJf3rgwOUfabUyzqhguMYb3sGBjOYqjevc=".into(), }, }, @@ -405,7 +419,7 @@ mod tests { .await .unwrap(); - let pin_key = derive_pin_key(&mut client, "1234".into()).unwrap(); + let pin_key = derive_pin_key(&mut client, SensitiveString::test("1234")).unwrap(); // Verify we can unlock with the pin let mut client2 = Client::new(None); @@ -418,7 +432,7 @@ mod tests { email: "test@bitwarden.com".into(), private_key: priv_key.to_owned(), method: InitUserCryptoMethod::Pin { - pin: "1234".into(), + pin: SensitiveString::test("1234"), pin_protected_user_key: pin_key.pin_protected_user_key, }, }, @@ -456,7 +470,7 @@ mod tests { email: "test@bitwarden.com".into(), private_key: priv_key.to_owned(), method: InitUserCryptoMethod::Pin { - pin: "1234".into(), + pin: SensitiveString::test("1234"), pin_protected_user_key, }, }, @@ -483,24 +497,26 @@ mod tests { #[cfg(feature = "internal")] #[test] fn test_enroll_admin_password_reset() { - use std::{num::NonZeroU32, ops::Deref}; + use std::num::NonZeroU32; - use base64::{engine::general_purpose::STANDARD, Engine}; - use bitwarden_crypto::AsymmetricCryptoKey; + use base64::engine::general_purpose::STANDARD; + use bitwarden_crypto::{AsymmetricCryptoKey, DecryptedString, DecryptedVec, SensitiveVec}; let mut client = Client::new(None); - client.set_login_method(LoginMethod::User(UserLoginMethod::Username { - client_id: "7b821276-e27c-400b-9853-606393c87f18".to_owned(), - email: "test@bitwarden.com".to_owned(), - kdf: Kdf::PBKDF2 { + + let master_key = bitwarden_crypto::MasterKey::derive( + &SensitiveVec::test(b"asdfasdfasdf"), + "test@bitwarden.com".as_bytes(), + &Kdf::PBKDF2 { iterations: NonZeroU32::new(600_000).unwrap(), }, - })); + ) + .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) + .initialize_user_crypto_master_key(master_key, user_key, private_key) .unwrap(); let public_key = "MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAsy7RFHcX3C8Q4/OMmhhbFReYWfB45W9PDTEA8tUZwZmtOiN2RErIS2M1c+K/4HoDJ/TjpbX1f2MZcr4nWvKFuqnZXyewFc+jmvKVewYi+NAu2++vqKq2kKcmMNhwoQDQdQIVy/Uqlp4Cpi2cIwO6ogq5nHNJGR3jm+CpyrafYlbz1bPvL3hbyoGDuG2tgADhyhXUdFuef2oF3wMvn1lAJAvJnPYpMiXUFmj1ejmbwtlxZDrHgUJvUcp7nYdwUKaFoi+sOttHn3u7eZPtNvxMjhSS/X/1xBIzP/mKNLdywH5LoRxniokUk+fV3PYUxJsiU3lV0Trc/tH46jqd8ZGjmwIDAQAB"; @@ -508,18 +524,16 @@ mod tests { let encrypted = enroll_admin_password_reset(&mut client, public_key.to_owned()).unwrap(); let private_key = "MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQCzLtEUdxfcLxDj84yaGFsVF5hZ8Hjlb08NMQDy1RnBma06I3ZESshLYzVz4r/gegMn9OOltfV/Yxlyvida8oW6qdlfJ7AVz6Oa8pV7BiL40C7b76+oqraQpyYw2HChANB1AhXL9SqWngKmLZwjA7qiCrmcc0kZHeOb4KnKtp9iVvPVs+8veFvKgYO4ba2AAOHKFdR0W55/agXfAy+fWUAkC8mc9ikyJdQWaPV6OZvC2XFkOseBQm9Rynudh3BQpoWiL6w620efe7t5k+02/EyOFJL9f/XEEjM/+Yo0t3LAfkuhHGeKiRST59Xc9hTEmyJTeVXROtz+0fjqOp3xkaObAgMBAAECggEACs4xhnO0HaZhh1/iH7zORMIRXKeyxP2LQiTR8xwN5JJ9wRWmGAR9VasS7EZFTDidIGVME2u/h4s5EqXnhxfO+0gGksVvgNXJ/qw87E8K2216g6ZNo6vSGA7H1GH2voWwejJ4/k/cJug6dz2S402rRAKh2Wong1arYHSkVlQp3diiMa5FHAOSE+Cy09O2ZsaF9IXQYUtlW6AVXFrBEPYH2kvkaPXchh8VETMijo6tbvoKLnUHe+wTaDMls7hy8exjtVyI59r3DNzjy1lNGaGb5QSnFMXR+eHhPZc844Wv02MxC15zKABADrl58gpJyjTl6XpDdHCYGsmGpVGH3X9TQQKBgQDz/9beFjzq59ve6rGwn+EtnQfSsyYT+jr7GN8lNEXb3YOFXBgPhfFIcHRh2R00Vm9w2ApfAx2cd8xm2I6HuvQ1Os7g26LWazvuWY0Qzb+KaCLQTEGH1RnTq6CCG+BTRq/a3J8M4t38GV5TWlzv8wr9U4dl6FR4efjb65HXs1GQ4QKBgQC7/uHfrOTEHrLeIeqEuSl0vWNqEotFKdKLV6xpOvNuxDGbgW4/r/zaxDqt0YBOXmRbQYSEhmO3oy9J6XfE1SUln0gbavZeW0HESCAmUIC88bDnspUwS9RxauqT5aF8ODKN/bNCWCnBM1xyonPOs1oT1nyparJVdQoG//Y7vkB3+wKBgBqLqPq8fKAp3XfhHLfUjREDVoiLyQa/YI9U42IOz9LdxKNLo6p8rgVthpvmnRDGnpUuS+KOWjhdqDVANjF6G3t3DG7WNl8Rh5Gk2H4NhFswfSkgQrjebFLlBy9gjQVCWXt8KSmjvPbiY6q52Aaa8IUjA0YJAregvXxfopxO+/7BAoGARicvEtDp7WWnSc1OPoj6N14VIxgYcI7SyrzE0d/1x3ffKzB5e7qomNpxKzvqrVP8DzG7ydh8jaKPmv1MfF8tpYRy3AhmN3/GYwCnPqT75YYrhcrWcVdax5gmQVqHkFtIQkRSCIftzPLlpMGKha/YBV8c1fvC4LD0NPh/Ynv0gtECgYEAyOZg95/kte0jpgUEgwuMrzkhY/AaUJULFuR5MkyvReEbtSBQwV5tx60+T95PHNiFooWWVXiLMsAgyI2IbkxVR1Pzdri3gWK5CTfqb7kLuaj/B7SGvBa2Sxo478KS5K8tBBBWkITqo+wLC0mn3uZi1dyMWO1zopTA+KtEGF2dtGQ="; + let private_key = DecryptedString::test(private_key); let private_key = - AsymmetricCryptoKey::from_der(&STANDARD.decode(private_key).unwrap()).unwrap(); - let decrypted: Vec = encrypted.decrypt_with_key(&private_key).unwrap(); + AsymmetricCryptoKey::from_der(private_key.decode_base64(STANDARD).unwrap()).unwrap(); + let decrypted: DecryptedVec = encrypted.decrypt_with_key(&private_key).unwrap(); let expected = client .get_encryption_settings() .unwrap() .get_key(&None) - .unwrap() - .to_vec() - .deref() - .clone(); - assert_eq!(decrypted, expected); + .unwrap(); + assert_eq!(decrypted, expected.to_vec()); } } diff --git a/crates/bitwarden/src/mobile/kdf.rs b/crates/bitwarden/src/mobile/kdf.rs index 1c1972086..b0c48c499 100644 --- a/crates/bitwarden/src/mobile/kdf.rs +++ b/crates/bitwarden/src/mobile/kdf.rs @@ -1,15 +1,16 @@ -use bitwarden_crypto::{HashPurpose, Kdf, MasterKey}; +use bitwarden_crypto::{HashPurpose, Kdf, MasterKey, SensitiveString}; use crate::{error::Result, Client}; pub async fn hash_password( _client: &Client, email: String, - password: String, + password: SensitiveString, kdf_params: Kdf, purpose: HashPurpose, -) -> Result { - let master_key = MasterKey::derive(password.as_bytes(), email.as_bytes(), &kdf_params)?; +) -> Result { + let password_vec = password.into(); + let master_key = MasterKey::derive(&password_vec, email.as_bytes(), &kdf_params)?; - Ok(master_key.derive_master_key_hash(password.as_bytes(), purpose)?) + Ok(master_key.derive_master_key_hash(&password_vec, purpose)?) } diff --git a/crates/bitwarden/src/mobile/vault/client_attachments.rs b/crates/bitwarden/src/mobile/vault/client_attachments.rs index e40721b04..cfe23d414 100644 --- a/crates/bitwarden/src/mobile/vault/client_attachments.rs +++ b/crates/bitwarden/src/mobile/vault/client_attachments.rs @@ -65,6 +65,7 @@ impl<'a> ClientAttachments<'a> { } .decrypt_with_key(key) .map_err(Error::Crypto) + .map(|s| s.expose().to_owned()) } pub async fn decrypt_file( &self, diff --git a/crates/bitwarden/src/mobile/vault/client_ciphers.rs b/crates/bitwarden/src/mobile/vault/client_ciphers.rs index c35cf3080..98a90ea15 100644 --- a/crates/bitwarden/src/mobile/vault/client_ciphers.rs +++ b/crates/bitwarden/src/mobile/vault/client_ciphers.rs @@ -1,4 +1,5 @@ -use bitwarden_crypto::{Decryptable, Encryptable, LocateKey}; +use bitwarden_crypto::{CryptoError, KeyDecryptable, KeyEncryptable, LocateKey}; +use uuid::Uuid; use super::client_vault::ClientVault; use crate::{ @@ -24,26 +25,39 @@ impl<'a> ClientCiphers<'a> { cipher_view.generate_cipher_key(key)?; } - let cipher = cipher_view.encrypt(enc, &None)?; + let key = enc.get_key(&None).ok_or(CryptoError::MissingKey)?; + let cipher = cipher_view.encrypt_with_key(key)?; Ok(cipher) } pub async fn decrypt(&self, cipher: Cipher) -> Result { let enc = self.client.get_encryption_settings()?; + let key = enc.get_key(&None).ok_or(CryptoError::MissingKey)?; - let cipher_view = cipher.decrypt(enc, &None)?; + let cipher_view = cipher.decrypt_with_key(key)?; Ok(cipher_view) } pub async fn decrypt_list(&self, ciphers: Vec) -> Result> { let enc = self.client.get_encryption_settings()?; + let key = enc.get_key(&None).ok_or(CryptoError::MissingKey)?; - let cipher_views = ciphers.decrypt(enc, &None)?; + let cipher_views = ciphers.decrypt_with_key(key)?; Ok(cipher_views) } + + pub async fn move_to_organization( + &self, + mut cipher_view: CipherView, + organization_id: Uuid, + ) -> Result { + let enc = self.client.get_encryption_settings()?; + cipher_view.move_to_organization(enc, organization_id)?; + Ok(cipher_view) + } } impl<'a> ClientVault<'a> { diff --git a/crates/bitwarden/src/mobile/vault/client_collection.rs b/crates/bitwarden/src/mobile/vault/client_collection.rs index 9cb5d1711..b2f541f32 100644 --- a/crates/bitwarden/src/mobile/vault/client_collection.rs +++ b/crates/bitwarden/src/mobile/vault/client_collection.rs @@ -1,4 +1,4 @@ -use bitwarden_crypto::Decryptable; +use bitwarden_crypto::{CryptoError, KeyDecryptable}; use super::client_vault::ClientVault; use crate::{ @@ -14,16 +14,18 @@ pub struct ClientCollections<'a> { impl<'a> ClientCollections<'a> { pub async fn decrypt(&self, collection: Collection) -> Result { let enc = self.client.get_encryption_settings()?; + let key = enc.get_key(&None).ok_or(CryptoError::MissingKey)?; - let view = collection.decrypt(enc, &None)?; + let view = collection.decrypt_with_key(key)?; Ok(view) } pub async fn decrypt_list(&self, collections: Vec) -> Result> { let enc = self.client.get_encryption_settings()?; + let key = enc.get_key(&None).ok_or(CryptoError::MissingKey)?; - let views = collections.decrypt(enc, &None)?; + let views = collections.decrypt_with_key(key)?; Ok(views) } diff --git a/crates/bitwarden/src/mobile/vault/client_folders.rs b/crates/bitwarden/src/mobile/vault/client_folders.rs index fec3ad7db..503c41906 100644 --- a/crates/bitwarden/src/mobile/vault/client_folders.rs +++ b/crates/bitwarden/src/mobile/vault/client_folders.rs @@ -1,4 +1,4 @@ -use bitwarden_crypto::{Decryptable, Encryptable}; +use bitwarden_crypto::{CryptoError, KeyDecryptable, KeyEncryptable}; use super::client_vault::ClientVault; use crate::{ @@ -14,24 +14,27 @@ pub struct ClientFolders<'a> { impl<'a> ClientFolders<'a> { pub async fn encrypt(&self, folder_view: FolderView) -> Result { let enc = self.client.get_encryption_settings()?; + let key = enc.get_key(&None).ok_or(CryptoError::MissingKey)?; - let folder = folder_view.encrypt(enc, &None)?; + let folder = folder_view.encrypt_with_key(key)?; Ok(folder) } pub async fn decrypt(&self, folder: Folder) -> Result { let enc = self.client.get_encryption_settings()?; + let key = enc.get_key(&None).ok_or(CryptoError::MissingKey)?; - let folder_view = folder.decrypt(enc, &None)?; + let folder_view = folder.decrypt_with_key(key)?; Ok(folder_view) } pub async fn decrypt_list(&self, folders: Vec) -> Result> { let enc = self.client.get_encryption_settings()?; + let key = enc.get_key(&None).ok_or(CryptoError::MissingKey)?; - let views = folders.decrypt(enc, &None)?; + let views = folders.decrypt_with_key(key)?; Ok(views) } diff --git a/crates/bitwarden/src/mobile/vault/client_password_history.rs b/crates/bitwarden/src/mobile/vault/client_password_history.rs index 99727232b..a9cba7433 100644 --- a/crates/bitwarden/src/mobile/vault/client_password_history.rs +++ b/crates/bitwarden/src/mobile/vault/client_password_history.rs @@ -1,4 +1,4 @@ -use bitwarden_crypto::{Decryptable, Encryptable}; +use bitwarden_crypto::{CryptoError, KeyDecryptable, KeyEncryptable}; use super::client_vault::ClientVault; use crate::{ @@ -14,8 +14,9 @@ pub struct ClientPasswordHistory<'a> { impl<'a> ClientPasswordHistory<'a> { pub async fn encrypt(&self, history_view: PasswordHistoryView) -> Result { let enc = self.client.get_encryption_settings()?; + let key = enc.get_key(&None).ok_or(CryptoError::MissingKey)?; - let history = history_view.encrypt(enc, &None)?; + let history = history_view.encrypt_with_key(key)?; Ok(history) } @@ -25,8 +26,9 @@ impl<'a> ClientPasswordHistory<'a> { history: Vec, ) -> Result> { let enc = self.client.get_encryption_settings()?; + let key = enc.get_key(&None).ok_or(CryptoError::MissingKey)?; - let history_view = history.decrypt(enc, &None)?; + let history_view = history.decrypt_with_key(key)?; Ok(history_view) } diff --git a/crates/bitwarden/src/mobile/vault/client_sends.rs b/crates/bitwarden/src/mobile/vault/client_sends.rs index e03432313..bc0d08e88 100644 --- a/crates/bitwarden/src/mobile/vault/client_sends.rs +++ b/crates/bitwarden/src/mobile/vault/client_sends.rs @@ -1,6 +1,6 @@ use std::path::Path; -use bitwarden_crypto::{Decryptable, EncString, Encryptable, KeyDecryptable, KeyEncryptable}; +use bitwarden_crypto::{DecryptedVec, EncString, KeyDecryptable, KeyEncryptable}; use super::client_vault::ClientVault; use crate::{ @@ -16,16 +16,18 @@ pub struct ClientSends<'a> { impl<'a> ClientSends<'a> { pub async fn decrypt(&self, send: Send) -> Result { let enc = self.client.get_encryption_settings()?; + let key = enc.get_key(&None).ok_or(Error::VaultLocked)?; - let send_view = send.decrypt(enc, &None)?; + let send_view = send.decrypt_with_key(key)?; Ok(send_view) } pub async fn decrypt_list(&self, sends: Vec) -> Result> { let enc = self.client.get_encryption_settings()?; + let key = enc.get_key(&None).ok_or(Error::VaultLocked)?; - let send_views = sends.decrypt(enc, &None)?; + let send_views = sends.decrypt_with_key(key)?; Ok(send_views) } @@ -48,13 +50,15 @@ impl<'a> ClientSends<'a> { let key = Send::get_key(&send.key, key)?; let buf = EncString::from_buffer(encrypted_buffer)?; - Ok(buf.decrypt_with_key(&key)?) + let dec: DecryptedVec = buf.decrypt_with_key(&key)?; + Ok(dec.expose().to_owned()) } pub async fn encrypt(&self, send_view: SendView) -> Result { let enc = self.client.get_encryption_settings()?; + let key = enc.get_key(&None).ok_or(Error::VaultLocked)?; - let send = send_view.encrypt(enc, &None)?; + let send = send_view.encrypt_with_key(key)?; Ok(send) } diff --git a/crates/bitwarden/src/platform/client_platform.rs b/crates/bitwarden/src/platform/client_platform.rs index ada8b92b8..91c15c358 100644 --- a/crates/bitwarden/src/platform/client_platform.rs +++ b/crates/bitwarden/src/platform/client_platform.rs @@ -1,6 +1,6 @@ use super::{ generate_fingerprint::{generate_fingerprint, generate_user_fingerprint}, - FingerprintRequest, FingerprintResponse, + ClientFido2, FingerprintRequest, FingerprintResponse, }; use crate::{error::Result, Client}; @@ -16,6 +16,14 @@ impl<'a> ClientPlatform<'a> { pub fn user_fingerprint(self, fingerprint_material: String) -> Result { generate_user_fingerprint(self.client, fingerprint_material) } + + /// At the moment this is just a stub implementation that doesn't do anything. It's here to make + /// it possible to check the usability API on the native clients. + pub fn fido2(&'a mut self) -> ClientFido2<'a> { + ClientFido2 { + client: self.client, + } + } } impl<'a> Client { diff --git a/crates/bitwarden/src/platform/domain.rs b/crates/bitwarden/src/platform/domain.rs index c903dd5a5..482cb1f59 100644 --- a/crates/bitwarden/src/platform/domain.rs +++ b/crates/bitwarden/src/platform/domain.rs @@ -1,7 +1,7 @@ use schemars::JsonSchema; use serde::{Deserialize, Serialize}; -use crate::error::{Error, Result}; +use crate::error::{require, Error, Result}; #[derive(Serialize, Deserialize, Debug, JsonSchema)] pub struct GlobalDomains { @@ -15,9 +15,9 @@ impl TryFrom for GlobalDomains { fn try_from(global_domains: bitwarden_api_api::models::GlobalDomains) -> Result { Ok(Self { - r#type: global_domains.r#type.ok_or(Error::MissingFields)?, - domains: global_domains.domains.ok_or(Error::MissingFields)?, - excluded: global_domains.excluded.ok_or(Error::MissingFields)?, + r#type: require!(global_domains.r#type), + domains: require!(global_domains.domains), + excluded: require!(global_domains.excluded), }) } } diff --git a/crates/bitwarden/src/platform/fido2/authenticator.rs b/crates/bitwarden/src/platform/fido2/authenticator.rs new file mode 100644 index 000000000..9f35a682d --- /dev/null +++ b/crates/bitwarden/src/platform/fido2/authenticator.rs @@ -0,0 +1,333 @@ +#![allow(dead_code, unused_mut, unused_imports, unused_variables)] + +use bitwarden_crypto::{EncString, KeyEncryptable, SensitiveString}; +use chrono::DateTime; +use passkey::{ + authenticator::{Authenticator, UserCheck}, + types::{ + ctap2::{make_credential::Request, Aaguid, Ctap2Error, StatusCode}, + Passkey, + }, +}; +use uuid::Uuid; + +use super::{CredentialStore, SelectedCredential, UserInterface}; +use crate::{ + error::{Error, Result}, + vault::{ + login::{Fido2CredentialView, LoginView}, + CipherView, Fido2Credential, + }, + Client, +}; + +pub struct Fido2Authenticator<'a> { + pub(crate) client: &'a mut Client, + pub(crate) user_interface: &'a dyn UserInterface, + pub(crate) credential_store: &'a dyn CredentialStore, +} + +impl<'a> Fido2Authenticator<'a> { + pub async fn make_credential( + &mut self, + request: MakeCredentialRequest, + ) -> Result { + // TODO: Placeholder value + let my_aaguid = Aaguid::new_empty(); + + let mut authenticator = Authenticator::new( + my_aaguid, + self.to_credential_store(), + self.to_user_interface(), + ); + + /*let response = authenticator + .make_credential(Request { + client_data_hash: request.client_data_hash.into(), + rp: passkey::types::ctap2::make_credential::PublicKeyCredentialRpEntity { + id: request.rp.id, + name: request.rp.name, + }, + user: passkey::types::webauthn::PublicKeyCredentialUserEntity { + id: request.user.id.into(), + display_name: request.user.display_name, + name: request.user.name, + }, + pub_key_cred_params: request + .pub_key_cred_params + .into_iter() + .map( + |x| passkey::types::webauthn::PublicKeyCredentialParameters { + ty: todo!(), + alg: todo!(), + }, + ) + .collect(), + exclude_list: request + .exclude_list + .map(|x: Vec| { + x.into_iter() + .map( + |x| passkey::types::webauthn::PublicKeyCredentialDescriptor { + ty: todo!(), + id: todo!(), + transports: None, + }, + ) + .collect() + }), + extensions: None, // TODO: request.extensions, + options: passkey::types::ctap2::make_credential::Options { + rk: true, + up: true, + uv: true, + }, + pin_auth: None, + pin_protocol: None, + }) + .await; + + let response = match response { + Ok(x) => x, + Err(e) => return Err(format!("make_credential error: {e:?}").into()), + }; + + Ok(MakeCredentialResult { + credential_id: response + .auth_data + .attested_credential_data + .expect("Missing attested_credential_data") + .credential_id() + .to_vec(), + })*/ + + Ok(MakeCredentialResult { + credential_id: vec![], + }) + } + + pub async fn get_assertion( + &mut self, + request: GetAssertionRequest, + ) -> Result { + let enc = self.client.get_encryption_settings()?; + let key = enc.get_key(&None).ok_or(Error::VaultLocked)?; + + Ok(GetAssertionResult { + credential_id: vec![], + authenticator_data: vec![], + signature: vec![], + user_handle: vec![], + selected_credential: SelectedCredential { + cipher: CipherView { + id: Some(Uuid::new_v4()), + organization_id: None, + folder_id: None, + collection_ids: vec![], + key: None, + name: SensitiveString::new(Box::new("".to_string())), + notes: Some(SensitiveString::new(Box::new("".to_string()))), + r#type: crate::vault::CipherType::Login, + login: Some(LoginView { + username: None, + password: None, + password_revision_date: None, + uris: None, + totp: None, + autofill_on_page_load: None, + fido2_credentials: Some(vec![]), + }), + identity: None, + card: None, + secure_note: None, + favorite: false, + reprompt: crate::vault::CipherRepromptType::None, + organization_use_totp: true, + edit: true, + view_password: true, + local_data: None, + attachments: Some(vec![]), + fields: Some(vec![]), + password_history: Some(vec![]), + creation_date: chrono::offset::Utc::now(), + deleted_date: None, + revision_date: chrono::offset::Utc::now(), + }, + credential: Fido2Credential { + credential_id: "".to_owned().encrypt_with_key(key)?, + key_type: "".to_owned().encrypt_with_key(key)?, + key_algorithm: "".to_owned().encrypt_with_key(key)?, + key_curve: "".to_owned().encrypt_with_key(key)?, + key_value: "".to_owned().encrypt_with_key(key)?, + rp_id: "".to_owned().encrypt_with_key(key)?, + user_handle: Some("".to_owned().encrypt_with_key(key)?), + user_name: Some("".to_owned().encrypt_with_key(key)?), + counter: "".to_owned().encrypt_with_key(key)?, + rp_name: Some("".to_owned().encrypt_with_key(key)?), + user_display_name: Some("".to_owned().encrypt_with_key(key)?), + discoverable: "".to_owned().encrypt_with_key(key)?, + creation_date: chrono::offset::Utc::now(), + }, + }, + }) + } + + // TODO: Fido2CredentialView contains all the fields, maybe we need a Fido2CredentialListView? + pub async fn silently_discover_credentials( + &mut self, + rp_id: String, + ) -> Result> { + Ok(vec![]) + } + + pub(crate) fn to_user_interface(&'a self) -> UserInterfaceImpl<'_> { + UserInterfaceImpl { + authenticator: self, + } + } + pub(crate) fn to_credential_store(&'a self) -> CredentialStoreImpl<'_> { + CredentialStoreImpl { + authenticator: self, + } + } +} + +pub(crate) struct CredentialStoreImpl<'a> { + authenticator: &'a Fido2Authenticator<'a>, +} +pub(crate) struct UserInterfaceImpl<'a> { + authenticator: &'a Fido2Authenticator<'a>, +} + +#[async_trait::async_trait] +impl passkey::authenticator::CredentialStore for CredentialStoreImpl<'_> { + type PasskeyItem = CipherView; + + async fn find_credentials( + &self, + ids: Option<&[passkey::types::webauthn::PublicKeyCredentialDescriptor]>, + rp_id: &str, + ) -> Result, StatusCode> { + Ok(vec![]) + } + + async fn save_credential( + &mut self, + cred: Passkey, + user: passkey::types::ctap2::make_credential::PublicKeyCredentialUserEntity, + rp: passkey::types::ctap2::make_credential::PublicKeyCredentialRpEntity, + ) -> Result<(), StatusCode> { + Ok(()) + } + + async fn update_credential(&mut self, cred: Passkey) -> Result<(), StatusCode> { + Ok(()) + } +} + +#[async_trait::async_trait] +impl passkey::authenticator::UserValidationMethod for UserInterfaceImpl<'_> { + type PasskeyItem = CipherView; + + async fn check_user( + &self, + credential: Option, + presence: bool, + verification: bool, + ) -> Result { + Ok(UserCheck { + presence, + verification, + }) + } + + fn is_presence_enabled(&self) -> bool { + true + } + + fn is_verification_enabled(&self) -> Option { + Some(true) + } +} + +// What type do we need this to be? We probably can't use Serialize over the FFI boundary +pub type Extensions = Option>; + +#[cfg_attr(feature = "mobile", derive(uniffi::Record))] +pub struct PublicKeyCredentialRpEntity { + pub id: String, + pub name: Option, +} + +#[cfg_attr(feature = "mobile", derive(uniffi::Record))] +pub struct PublicKeyCredentialUserEntity { + pub id: Vec, + pub display_name: String, + pub name: String, +} + +#[cfg_attr(feature = "mobile", derive(uniffi::Record))] +pub struct PublicKeyCredentialParameters { + pub ty: String, + pub alg: i64, +} + +#[cfg_attr(feature = "mobile", derive(uniffi::Record))] +pub struct PublicKeyCredentialDescriptor { + pub ty: i64, + pub id: Vec, +} + +#[cfg_attr(feature = "mobile", derive(uniffi::Record))] +pub struct MakeCredentialRequest { + client_data_hash: Vec, + rp: PublicKeyCredentialRpEntity, + user: PublicKeyCredentialUserEntity, + pub_key_cred_params: Vec, + exclude_list: Option>, + require_resident_key: bool, + extensions: Extensions, +} + +#[cfg_attr(feature = "mobile", derive(uniffi::Record))] +pub struct MakeCredentialResult { + // TODO + // authenticator_data: Vec, + // attested_credential_data: Vec, + credential_id: Vec, +} + +#[cfg_attr(feature = "mobile", derive(uniffi::Record))] +pub struct GetAssertionRequest { + rp_id: String, + client_data_hash: Vec, + allow_list: Option>, + options: Options, + extensions: Extensions, +} + +#[cfg_attr(feature = "mobile", derive(uniffi::Record))] +pub struct Options { + rk: bool, + uv: UV, +} + +#[cfg_attr(feature = "mobile", derive(uniffi::Enum))] +pub enum UV { + Discouraged, + Preferred, + Required, +} + +#[cfg_attr(feature = "mobile", derive(uniffi::Record))] +pub struct GetAssertionResult { + credential_id: Vec, + authenticator_data: Vec, + signature: Vec, + user_handle: Vec, + /** + * SDK IMPL NOTE: This is not part of the spec and is not returned by passkey-rs. + * The SDK needs to add this after the response from passkey-rs is received. + */ + selected_credential: SelectedCredential, +} diff --git a/crates/bitwarden/src/platform/fido2/client.rs b/crates/bitwarden/src/platform/fido2/client.rs new file mode 100644 index 000000000..a175e17bb --- /dev/null +++ b/crates/bitwarden/src/platform/fido2/client.rs @@ -0,0 +1,315 @@ +#![allow(dead_code, unused_variables)] + +use std::collections::HashMap; + +use bitwarden_crypto::{KeyEncryptable, SensitiveString}; +use passkey::{authenticator::Authenticator, types::ctap2::Aaguid}; +use reqwest::Url; +use serde::Serialize; +use uuid::Uuid; + +use super::{Fido2Authenticator, SelectedCredential}; +use crate::{ + error::{Error, Result}, + vault::{login::LoginView, CipherView, Fido2Credential}, +}; + +pub struct Fido2Client<'a> { + pub(crate) authenticator: Fido2Authenticator<'a>, +} + +impl<'a> Fido2Client<'a> { + pub async fn register( + &mut self, + origin: String, + request: String, + client_data: ClientData, + ) -> Result { + // TODO: Placeholder value + let my_aaguid = Aaguid::new_empty(); + + let authenticator = Authenticator::new( + my_aaguid, + self.authenticator.to_credential_store(), + self.authenticator.to_user_interface(), + ); + let mut client = passkey::client::Client::new(authenticator); + + let origin = Url::parse(&origin).expect("Invalid URL"); + + let result = client + .register(&origin, serde_json::from_str(&request)?, client_data) + .await?; + + /*Ok(PublicKeyCredentialAuthenticatorAttestationResponse { + id: result.id, + raw_id: result.raw_id.into(), + ty: "public-key".to_string(), + authenticator_attachment: todo!(), + client_extension_results: todo!(), + response: AuthenticatorAttestationResponse { + client_data_json: result.response.client_data_json.into(), + authenticator_data: result.response.authenticator_data.into(), + public_key: result.response.public_key.map(|x| x.into()), + public_key_algorithm: result.response.public_key_algorithm, + attestation_object: result.response.attestation_object.into(), + transports: todo!(), + }, + selected_credential: SelectedCredential { + cipher: todo!(), + credential: todo!(), + }, + })*/ + let enc = self.authenticator.client.get_encryption_settings()?; + let key = enc.get_key(&None).ok_or(Error::VaultLocked)?; + + Ok(PublicKeyCredentialAuthenticatorAttestationResponse { + id: String::new(), + raw_id: vec![], + ty: "public-key".to_string(), + authenticator_attachment: String::new(), + client_extension_results: HashMap::new(), + response: AuthenticatorAttestationResponse { + client_data_json: vec![], + authenticator_data: vec![], + public_key: None, + public_key_algorithm: 0, + attestation_object: vec![], + transports: None, + }, + selected_credential: SelectedCredential { + cipher: CipherView { + id: Some(Uuid::new_v4()), + organization_id: None, + folder_id: None, + collection_ids: vec![], + key: None, + name: SensitiveString::new(Box::new("".to_string())), + notes: Some(SensitiveString::new(Box::new("".to_string()))), + r#type: crate::vault::CipherType::Login, + login: Some(LoginView { + username: None, + password: None, + password_revision_date: None, + uris: None, + totp: None, + autofill_on_page_load: None, + fido2_credentials: Some(vec![]), + }), + identity: None, + card: None, + secure_note: None, + favorite: false, + reprompt: crate::vault::CipherRepromptType::None, + organization_use_totp: true, + edit: true, + view_password: true, + local_data: None, + attachments: Some(vec![]), + fields: Some(vec![]), + password_history: Some(vec![]), + creation_date: chrono::offset::Utc::now(), + deleted_date: None, + revision_date: chrono::offset::Utc::now(), + }, + credential: Fido2Credential { + credential_id: "".to_owned().encrypt_with_key(key)?, + key_type: "".to_owned().encrypt_with_key(key)?, + key_algorithm: "".to_owned().encrypt_with_key(key)?, + key_curve: "".to_owned().encrypt_with_key(key)?, + key_value: "".to_owned().encrypt_with_key(key)?, + rp_id: "".to_owned().encrypt_with_key(key)?, + user_handle: Some("".to_owned().encrypt_with_key(key)?), + user_name: Some("".to_owned().encrypt_with_key(key)?), + counter: "".to_owned().encrypt_with_key(key)?, + rp_name: Some("".to_owned().encrypt_with_key(key)?), + user_display_name: Some("".to_owned().encrypt_with_key(key)?), + discoverable: "".to_owned().encrypt_with_key(key)?, + creation_date: chrono::offset::Utc::now(), + }, + }, + }) + } + pub async fn authenticate( + &mut self, + origin: String, + request: String, + client_data: ClientData, + ) -> Result { + // TODO: Placeholder value + let my_aaguid = Aaguid::new_empty(); + + let authenticator = Authenticator::new( + my_aaguid, + self.authenticator.to_credential_store(), + self.authenticator.to_user_interface(), + ); + let mut client = passkey::client::Client::new(authenticator); + + let origin = Url::parse(&origin).expect("Invalid URL"); + + let result = client + .authenticate(&origin, serde_json::from_str(&request)?, client_data) + .await?; + + /*Ok(PublicKeyCredentialAuthenticatorAssertionResponse { + id: result.id, + raw_id: result.raw_id.into(), + ty: "public-key".to_string(), + authenticator_attachment: todo!(), + client_extension_results: todo!(), + response: AuthenticatorAssertionResponse { + client_data_json: result.response.client_data_json.into(), + authenticator_data: result.response.authenticator_data.into(), + signature: result.response.signature.into(), + user_handle: result.response.user_handle.unwrap_or_default().into(), + }, + selected_credential: SelectedCredential { + cipher: todo!(), + credential: todo!(), + }, + })*/ + + let enc = self.authenticator.client.get_encryption_settings()?; + let key = enc.get_key(&None).ok_or(Error::VaultLocked)?; + + Ok(PublicKeyCredentialAuthenticatorAssertionResponse { + id: String::new(), + raw_id: vec![], + ty: "public-key".to_string(), + authenticator_attachment: String::new(), + client_extension_results: HashMap::new(), + response: AuthenticatorAssertionResponse { + client_data_json: vec![], + authenticator_data: vec![], + signature: vec![], + user_handle: vec![], + }, + selected_credential: SelectedCredential { + cipher: CipherView { + id: Some(Uuid::new_v4()), + organization_id: None, + folder_id: None, + collection_ids: vec![], + key: None, + name: SensitiveString::new(Box::new("".to_string())), + notes: Some(SensitiveString::new(Box::new("".to_string()))), + r#type: crate::vault::CipherType::Login, + login: Some(LoginView { + username: None, + password: None, + password_revision_date: None, + uris: None, + totp: None, + autofill_on_page_load: None, + fido2_credentials: Some(vec![]), + }), + identity: None, + card: None, + secure_note: None, + favorite: false, + reprompt: crate::vault::CipherRepromptType::None, + organization_use_totp: true, + edit: true, + view_password: true, + local_data: None, + attachments: Some(vec![]), + fields: Some(vec![]), + password_history: Some(vec![]), + creation_date: chrono::offset::Utc::now(), + deleted_date: None, + revision_date: chrono::offset::Utc::now(), + }, + credential: Fido2Credential { + credential_id: "".to_owned().encrypt_with_key(key)?, + key_type: "".to_owned().encrypt_with_key(key)?, + key_algorithm: "".to_owned().encrypt_with_key(key)?, + key_curve: "".to_owned().encrypt_with_key(key)?, + key_value: "".to_owned().encrypt_with_key(key)?, + rp_id: "".to_owned().encrypt_with_key(key)?, + user_handle: Some("".to_owned().encrypt_with_key(key)?), + user_name: Some("".to_owned().encrypt_with_key(key)?), + counter: "".to_owned().encrypt_with_key(key)?, + rp_name: Some("".to_owned().encrypt_with_key(key)?), + user_display_name: Some("".to_owned().encrypt_with_key(key)?), + discoverable: "".to_owned().encrypt_with_key(key)?, + creation_date: chrono::offset::Utc::now(), + }, + }, + }) + } +} + +#[cfg_attr(feature = "mobile", derive(uniffi::Enum))] +pub enum ClientData { + DefaultWithExtraData { android_package_name: String }, + DefaultWithCustomHash { hash: Vec }, +} + +#[derive(Serialize, Clone)] +struct AndroidClientData { + android_package_name: String, +} + +// TODO: I'm implementing this to convert from a basic enum into the generic +// passkey::client::ClientData Not fully sure that it's correct to return None for extra_client_data +// instead of () +impl passkey::client::ClientData> for ClientData { + fn extra_client_data(&self) -> Option { + match self { + ClientData::DefaultWithExtraData { + android_package_name, + } => Some(AndroidClientData { + android_package_name: android_package_name.clone(), + }), + ClientData::DefaultWithCustomHash { .. } => None, + } + } + + fn client_data_hash(&self) -> Option> { + match self { + ClientData::DefaultWithExtraData { .. } => None, + ClientData::DefaultWithCustomHash { hash } => Some(hash.clone()), + } + } +} + +#[cfg_attr(feature = "mobile", derive(uniffi::Record))] +pub struct PublicKeyCredentialAuthenticatorAttestationResponse { + id: String, + raw_id: Vec, + ty: String, + authenticator_attachment: String, + client_extension_results: HashMap, + response: AuthenticatorAttestationResponse, + selected_credential: SelectedCredential, +} + +#[cfg_attr(feature = "mobile", derive(uniffi::Record))] +pub struct AuthenticatorAttestationResponse { + client_data_json: Vec, + authenticator_data: Vec, + public_key: Option>, + public_key_algorithm: i64, + attestation_object: Vec, + transports: Option>, +} + +#[cfg_attr(feature = "mobile", derive(uniffi::Record))] +pub struct PublicKeyCredentialAuthenticatorAssertionResponse { + id: String, + raw_id: Vec, + ty: String, + authenticator_attachment: String, + client_extension_results: HashMap, + response: AuthenticatorAssertionResponse, + selected_credential: SelectedCredential, +} + +#[cfg_attr(feature = "mobile", derive(uniffi::Record))] +pub struct AuthenticatorAssertionResponse { + client_data_json: Vec, + authenticator_data: Vec, + signature: Vec, + user_handle: Vec, +} diff --git a/crates/bitwarden/src/platform/fido2/mod.rs b/crates/bitwarden/src/platform/fido2/mod.rs new file mode 100644 index 000000000..38dc965cb --- /dev/null +++ b/crates/bitwarden/src/platform/fido2/mod.rs @@ -0,0 +1,72 @@ +use crate::{ + error::Result, + vault::{login::Fido2Credential, CipherView}, + Client, +}; + +mod authenticator; +mod client; +mod traits; + +pub use authenticator::{ + Fido2Authenticator, GetAssertionRequest, GetAssertionResult, MakeCredentialRequest, + MakeCredentialResult, +}; +pub use client::{ + AuthenticatorAssertionResponse, AuthenticatorAttestationResponse, ClientData, Fido2Client, + PublicKeyCredentialAuthenticatorAssertionResponse, + PublicKeyCredentialAuthenticatorAttestationResponse, +}; +use passkey::types::Passkey; +pub use traits::{CheckUserOptions, CheckUserResult, CredentialStore, UserInterface, Verification}; + +pub struct ClientFido2<'a> { + #[allow(dead_code)] + pub(crate) client: &'a mut Client, +} + +impl<'a> ClientFido2<'a> { + pub fn create_authenticator( + &'a mut self, + + user_interface: &'a dyn UserInterface, + credential_store: &'a dyn CredentialStore, + ) -> Result> { + Ok(Fido2Authenticator { + client: self.client, + user_interface, + credential_store, + }) + } + + pub fn create_client( + &'a mut self, + + user_interface: &'a dyn UserInterface, + credential_store: &'a dyn CredentialStore, + ) -> Result> { + Ok(Fido2Client { + authenticator: self.create_authenticator(user_interface, credential_store)?, + }) + } +} + +#[allow(dead_code)] +#[cfg_attr(feature = "mobile", derive(uniffi::Record))] +pub struct SelectedCredential { + cipher: CipherView, + credential: Fido2Credential, +} + +impl TryFrom for Passkey { + type Error = crate::error::Error; + + fn try_from(value: CipherView) -> std::prelude::v1::Result { + let _creds = value + .login + .and_then(|l| l.fido2_credentials) + .ok_or("No Fido2Credential")?; + + todo!("We have more than one credential, we need to pick one?") + } +} diff --git a/crates/bitwarden/src/platform/fido2/traits.rs b/crates/bitwarden/src/platform/fido2/traits.rs new file mode 100644 index 000000000..e7429e0a5 --- /dev/null +++ b/crates/bitwarden/src/platform/fido2/traits.rs @@ -0,0 +1,50 @@ +use crate::{ + error::Result, + vault::{login::Fido2Credential, Cipher, CipherView}, +}; + +#[async_trait::async_trait] +pub trait UserInterface: Send + Sync { + async fn check_user( + &self, + options: CheckUserOptions, + credential: Option, + ) -> Result; + async fn pick_credential_for_authentication( + &self, + available_credentials: Vec, + ) -> Result; + async fn pick_credential_for_creation( + &self, + available_credentials: Vec, + new_credential: Fido2Credential, + ) -> Result; +} + +#[async_trait::async_trait] +pub trait CredentialStore: Send + Sync { + async fn find_credentials( + &self, + ids: Option>>, + rip_id: String, + ) -> Result>; + + async fn save_credential(&self, cred: Cipher) -> Result<()>; +} + +#[cfg_attr(feature = "mobile", derive(uniffi::Enum))] +pub enum CheckUserOptions { + RequirePresence(bool), + RequireVerification(Verification), +} +#[cfg_attr(feature = "mobile", derive(uniffi::Enum))] +pub enum Verification { + Discouraged, + Preferred, + Required, +} + +pub struct CheckUserResult { + pub user_present: bool, + pub user_verified: bool, +} diff --git a/crates/bitwarden/src/platform/generate_fingerprint.rs b/crates/bitwarden/src/platform/generate_fingerprint.rs index 59d81d652..f6ffe0f36 100644 --- a/crates/bitwarden/src/platform/generate_fingerprint.rs +++ b/crates/bitwarden/src/platform/generate_fingerprint.rs @@ -54,11 +54,10 @@ pub(crate) fn generate_user_fingerprint( mod tests { use std::num::NonZeroU32; + use bitwarden_crypto::SensitiveVec; + use super::*; - use crate::{ - client::{Kdf, LoginMethod, UserLoginMethod}, - Client, - }; + use crate::{client::Kdf, Client}; #[test] fn test_generate_user_fingerprint() { @@ -67,16 +66,19 @@ mod tests { let fingerprint_material = "a09726a0-9590-49d1-a5f5-afe300b6a515"; let mut client = Client::new(None); - client.set_login_method(LoginMethod::User(UserLoginMethod::Username { - client_id: "a09726a0-9590-49d1-a5f5-afe300b6a515".to_owned(), - email: "robb@stark.com".to_owned(), - kdf: Kdf::PBKDF2 { + + let master_key = bitwarden_crypto::MasterKey::derive( + &SensitiveVec::test(b"asdfasdfasdf"), + "robb@stark.com".as_bytes(), + &Kdf::PBKDF2 { iterations: NonZeroU32::new(600_000).unwrap(), }, - })); + ) + .unwrap(); + client - .initialize_user_crypto( - "asdfasdfasdf", + .initialize_user_crypto_master_key( + master_key, user_key.parse().unwrap(), private_key.parse().unwrap(), ) diff --git a/crates/bitwarden/src/platform/get_user_api_key.rs b/crates/bitwarden/src/platform/get_user_api_key.rs index 2eaa21894..14e961da5 100644 --- a/crates/bitwarden/src/platform/get_user_api_key.rs +++ b/crates/bitwarden/src/platform/get_user_api_key.rs @@ -10,13 +10,13 @@ use serde::{Deserialize, Serialize}; use super::SecretVerificationRequest; use crate::{ client::{LoginMethod, UserLoginMethod}, - error::{Error, Result}, + error::{require, Error, Result}, Client, }; pub(crate) async fn get_user_api_key( client: &mut Client, - input: &SecretVerificationRequest, + input: SecretVerificationRequest, ) -> Result { info!("Getting Api Key"); debug!("{:?}", input); @@ -43,20 +43,19 @@ fn get_login_method(client: &Client) -> Result<&LoginMethod> { fn get_secret_verification_request( login_method: &LoginMethod, - input: &SecretVerificationRequest, + input: SecretVerificationRequest, ) -> Result { if let LoginMethod::User(UserLoginMethod::Username { email, kdf, .. }) = login_method { let master_password_hash = input .master_password - .as_ref() .map(|p| { - let master_key = MasterKey::derive(p.as_bytes(), email.as_bytes(), kdf)?; - - master_key.derive_master_key_hash(p.as_bytes(), HashPurpose::ServerAuthorization) + let password_vec = p.into(); + let master_key = MasterKey::derive(&password_vec, email.as_bytes(), kdf)?; + master_key.derive_master_key_hash(&password_vec, HashPurpose::ServerAuthorization) }) .transpose()?; Ok(SecretVerificationRequestModel { - master_password_hash, + master_password_hash: master_password_hash.map(|h| h.expose().clone()), otp: input.otp.as_ref().cloned(), secret: None, auth_request_access_code: None, @@ -75,9 +74,7 @@ pub struct UserApiKeyResponse { impl UserApiKeyResponse { pub(crate) fn process_response(response: ApiKeyResponseModel) -> Result { - match response.api_key { - Some(api_key) => Ok(UserApiKeyResponse { api_key }), - None => Err(Error::MissingFields), - } + let api_key = require!(response.api_key); + Ok(UserApiKeyResponse { api_key }) } } diff --git a/crates/bitwarden/src/platform/mod.rs b/crates/bitwarden/src/platform/mod.rs index b78cb27d4..a51b94b94 100644 --- a/crates/bitwarden/src/platform/mod.rs +++ b/crates/bitwarden/src/platform/mod.rs @@ -1,10 +1,12 @@ pub mod client_platform; mod domain; +pub mod fido2; mod generate_fingerprint; mod get_user_api_key; mod secret_verification_request; mod sync; +pub use fido2::{ClientFido2, Fido2Authenticator, Fido2Client}; pub use generate_fingerprint::{FingerprintRequest, FingerprintResponse}; pub(crate) use get_user_api_key::get_user_api_key; pub use get_user_api_key::UserApiKeyResponse; diff --git a/crates/bitwarden/src/platform/secret_verification_request.rs b/crates/bitwarden/src/platform/secret_verification_request.rs index e05926620..4fe23e286 100644 --- a/crates/bitwarden/src/platform/secret_verification_request.rs +++ b/crates/bitwarden/src/platform/secret_verification_request.rs @@ -1,3 +1,4 @@ +use bitwarden_crypto::SensitiveString; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; @@ -6,7 +7,7 @@ use serde::{Deserialize, Serialize}; pub struct SecretVerificationRequest { /// The user's master password to use for user verification. If supplied, this will be used for /// verification purposes. - pub master_password: Option, + 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. diff --git a/crates/bitwarden/src/platform/sync.rs b/crates/bitwarden/src/platform/sync.rs index c1a039137..5a5c6ee82 100644 --- a/crates/bitwarden/src/platform/sync.rs +++ b/crates/bitwarden/src/platform/sync.rs @@ -9,7 +9,7 @@ use super::domain::GlobalDomains; use crate::{ admin_console::Policy, client::{encryption_settings::EncryptionSettings, Client}, - error::{Error, Result}, + error::{require, Error, Result}, vault::{Cipher, Collection, Folder}, }; @@ -25,10 +25,7 @@ pub(crate) async fn sync(client: &mut Client, input: &SyncRequest) -> Result = sync - .profile - .as_ref() - .ok_or(Error::MissingFields)? + let org_keys: Vec<_> = require!(sync.profile.as_ref()) .organizations .as_deref() .unwrap_or_default() @@ -86,8 +83,8 @@ impl SyncResponse { response: SyncResponseModel, enc: &EncryptionSettings, ) -> Result { - let profile = *response.profile.ok_or(Error::MissingFields)?; - let ciphers = response.ciphers.ok_or(Error::MissingFields)?; + let profile = require!(response.profile); + let ciphers = require!(response.ciphers); fn try_into_iter(iter: In) -> Result where @@ -99,13 +96,13 @@ impl SyncResponse { } Ok(SyncResponse { - profile: ProfileResponse::process_response(profile, enc)?, - folders: try_into_iter(response.folders.ok_or(Error::MissingFields)?)?, - collections: try_into_iter(response.collections.ok_or(Error::MissingFields)?)?, + profile: ProfileResponse::process_response(*profile, enc)?, + folders: try_into_iter(require!(response.folders))?, + collections: try_into_iter(require!(response.collections))?, ciphers: try_into_iter(ciphers)?, domains: response.domains.map(|d| (*d).try_into()).transpose()?, - policies: try_into_iter(response.policies.ok_or(Error::MissingFields)?)?, - sends: try_into_iter(response.sends.ok_or(Error::MissingFields)?)?, + policies: try_into_iter(require!(response.policies))?, + sends: try_into_iter(require!(response.sends))?, }) } } @@ -115,7 +112,7 @@ impl ProfileOrganizationResponse { response: ProfileOrganizationResponseModel, ) -> Result { Ok(ProfileOrganizationResponse { - id: response.id.ok_or(Error::MissingFields)?, + id: require!(response.id), }) } } @@ -126,9 +123,9 @@ impl ProfileResponse { _enc: &EncryptionSettings, ) -> Result { Ok(ProfileResponse { - id: response.id.ok_or(Error::MissingFields)?, - name: response.name.ok_or(Error::MissingFields)?, - email: response.email.ok_or(Error::MissingFields)?, + id: require!(response.id), + name: require!(response.name), + email: require!(response.email), //key: response.key, //private_key: response.private_key, organizations: response diff --git a/crates/bitwarden/src/secrets_manager/projects/delete.rs b/crates/bitwarden/src/secrets_manager/projects/delete.rs index 55f792669..05c808c7e 100644 --- a/crates/bitwarden/src/secrets_manager/projects/delete.rs +++ b/crates/bitwarden/src/secrets_manager/projects/delete.rs @@ -7,7 +7,7 @@ use uuid::Uuid; use crate::{ client::Client, - error::{Error, Result}, + error::{require, Result}, }; #[derive(Serialize, Deserialize, Debug, JsonSchema)] @@ -62,7 +62,7 @@ impl ProjectDeleteResponse { response: BulkDeleteResponseModel, ) -> Result { Ok(ProjectDeleteResponse { - id: response.id.ok_or(Error::MissingFields)?, + id: require!(response.id), error: response.error, }) } diff --git a/crates/bitwarden/src/secrets_manager/projects/project_response.rs b/crates/bitwarden/src/secrets_manager/projects/project_response.rs index 1e6f6a158..bf56f2006 100644 --- a/crates/bitwarden/src/secrets_manager/projects/project_response.rs +++ b/crates/bitwarden/src/secrets_manager/projects/project_response.rs @@ -1,5 +1,5 @@ use bitwarden_api_api::models::ProjectResponseModel; -use bitwarden_crypto::{Decryptable, EncString}; +use bitwarden_crypto::{CryptoError, DecryptedString, EncString, KeyDecryptable}; use chrono::{DateTime, Utc}; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; @@ -7,7 +7,7 @@ use uuid::Uuid; use crate::{ client::encryption_settings::EncryptionSettings, - error::{Error, Result}, + error::{require, Result}, }; #[derive(Serialize, Deserialize, Debug, JsonSchema)] @@ -25,27 +25,22 @@ impl ProjectResponse { response: ProjectResponseModel, enc: &EncryptionSettings, ) -> Result { - let organization_id = response.organization_id.ok_or(Error::MissingFields)?; + let organization_id = require!(response.organization_id); + let enc_key = enc + .get_key(&Some(organization_id)) + .ok_or(CryptoError::MissingKey)?; - let name = response - .name - .ok_or(Error::MissingFields)? + let name: DecryptedString = require!(response.name) .parse::()? - .decrypt(enc, &Some(organization_id))?; + .decrypt_with_key(enc_key)?; Ok(ProjectResponse { - id: response.id.ok_or(Error::MissingFields)?, + id: require!(response.id), organization_id, - name, + name: name.expose().to_owned(), - creation_date: response - .creation_date - .ok_or(Error::MissingFields)? - .parse()?, - revision_date: response - .revision_date - .ok_or(Error::MissingFields)? - .parse()?, + creation_date: require!(response.creation_date).parse()?, + revision_date: require!(response.revision_date).parse()?, }) } } diff --git a/crates/bitwarden/src/secrets_manager/secrets/delete.rs b/crates/bitwarden/src/secrets_manager/secrets/delete.rs index 19337e7a1..f3fe264e1 100644 --- a/crates/bitwarden/src/secrets_manager/secrets/delete.rs +++ b/crates/bitwarden/src/secrets_manager/secrets/delete.rs @@ -7,7 +7,7 @@ use uuid::Uuid; use crate::{ client::Client, - error::{Error, Result}, + error::{require, Result}, }; #[derive(Serialize, Deserialize, Debug, JsonSchema)] @@ -62,7 +62,7 @@ impl SecretDeleteResponse { response: BulkDeleteResponseModel, ) -> Result { Ok(SecretDeleteResponse { - id: response.id.ok_or(Error::MissingFields)?, + id: require!(response.id), error: response.error, }) } diff --git a/crates/bitwarden/src/secrets_manager/secrets/list.rs b/crates/bitwarden/src/secrets_manager/secrets/list.rs index 7fa46d164..b2473dd88 100644 --- a/crates/bitwarden/src/secrets_manager/secrets/list.rs +++ b/crates/bitwarden/src/secrets_manager/secrets/list.rs @@ -1,14 +1,14 @@ use bitwarden_api_api::models::{ SecretWithProjectsListResponseModel, SecretsWithProjectsInnerSecret, }; -use bitwarden_crypto::{Decryptable, EncString}; +use bitwarden_crypto::{CryptoError, DecryptedString, EncString, KeyDecryptable}; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; use uuid::Uuid; use crate::{ client::{encryption_settings::EncryptionSettings, Client}, - error::{Error, Result}, + error::{require, Result}, }; #[derive(Serialize, Deserialize, Debug, JsonSchema)] @@ -93,18 +93,19 @@ impl SecretIdentifierResponse { response: SecretsWithProjectsInnerSecret, enc: &EncryptionSettings, ) -> Result { - let organization_id = response.organization_id.ok_or(Error::MissingFields)?; + let organization_id = require!(response.organization_id); + let enc_key = enc + .get_key(&Some(organization_id)) + .ok_or(CryptoError::MissingKey)?; - let key = response - .key - .ok_or(Error::MissingFields)? + let key: DecryptedString = require!(response.key) .parse::()? - .decrypt(enc, &Some(organization_id))?; + .decrypt_with_key(enc_key)?; Ok(SecretIdentifierResponse { - id: response.id.ok_or(Error::MissingFields)?, + id: require!(response.id), organization_id, - key, + key: key.expose().to_owned(), }) } } diff --git a/crates/bitwarden/src/secrets_manager/secrets/secret_response.rs b/crates/bitwarden/src/secrets_manager/secrets/secret_response.rs index fe1a4d342..6f7446362 100644 --- a/crates/bitwarden/src/secrets_manager/secrets/secret_response.rs +++ b/crates/bitwarden/src/secrets_manager/secrets/secret_response.rs @@ -1,7 +1,7 @@ use bitwarden_api_api::models::{ BaseSecretResponseModel, BaseSecretResponseModelListResponseModel, SecretResponseModel, }; -use bitwarden_crypto::{Decryptable, EncString}; +use bitwarden_crypto::{CryptoError, DecryptedString, EncString, KeyDecryptable}; use chrono::{DateTime, Utc}; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; @@ -9,7 +9,7 @@ use uuid::Uuid; use crate::{ client::encryption_settings::EncryptionSettings, - error::{Error, Result}, + error::{require, Result}, }; #[derive(Serialize, Deserialize, Debug, JsonSchema)] @@ -50,22 +50,17 @@ impl SecretResponse { enc: &EncryptionSettings, ) -> Result { let org_id = response.organization_id; + let enc_key = enc.get_key(&org_id).ok_or(CryptoError::MissingKey)?; - let key = response - .key - .ok_or(Error::MissingFields)? + let key: DecryptedString = require!(response.key) .parse::()? - .decrypt(enc, &org_id)?; - let value = response - .value - .ok_or(Error::MissingFields)? + .decrypt_with_key(enc_key)?; + let value: DecryptedString = require!(response.value) .parse::()? - .decrypt(enc, &org_id)?; - let note = response - .note - .ok_or(Error::MissingFields)? + .decrypt_with_key(enc_key)?; + let note: DecryptedString = require!(response.note) .parse::()? - .decrypt(enc, &org_id)?; + .decrypt_with_key(enc_key)?; let project = response .projects @@ -73,21 +68,15 @@ impl SecretResponse { .and_then(|p| p.id); Ok(SecretResponse { - id: response.id.ok_or(Error::MissingFields)?, - organization_id: org_id.ok_or(Error::MissingFields)?, + id: require!(response.id), + organization_id: require!(org_id), project_id: project, - key, - value, - note, + key: key.expose().to_owned(), + value: value.expose().to_owned(), + note: note.expose().to_owned(), - creation_date: response - .creation_date - .ok_or(Error::MissingFields)? - .parse()?, - revision_date: response - .revision_date - .ok_or(Error::MissingFields)? - .parse()?, + creation_date: require!(response.creation_date).parse()?, + revision_date: require!(response.revision_date).parse()?, }) } } diff --git a/crates/bitwarden/src/secrets_manager/state.rs b/crates/bitwarden/src/secrets_manager/state.rs index b2b6f6a8e..aadea564d 100644 --- a/crates/bitwarden/src/secrets_manager/state.rs +++ b/crates/bitwarden/src/secrets_manager/state.rs @@ -1,6 +1,8 @@ use std::{fmt::Debug, path::Path}; -use bitwarden_crypto::{EncString, KeyDecryptable, KeyEncryptable}; +use bitwarden_crypto::{ + DecryptedString, EncString, KeyDecryptable, KeyEncryptable, SensitiveString, +}; use serde::{Deserialize, Serialize}; use crate::{ @@ -15,11 +17,11 @@ const STATE_VERSION: u32 = 1; pub struct ClientState { pub(crate) version: u32, pub(crate) token: String, - pub(crate) encryption_key: String, + pub(crate) encryption_key: SensitiveString, } impl ClientState { - pub fn new(token: String, encryption_key: String) -> Self { + pub fn new(token: String, encryption_key: SensitiveString) -> Self { Self { version: STATE_VERSION, token, @@ -32,8 +34,9 @@ pub fn get(state_file: &Path, access_token: &AccessToken) -> Result let file_content = std::fs::read_to_string(state_file)?; let encrypted_state: EncString = file_content.parse()?; - let decrypted_state: String = encrypted_state.decrypt_with_key(&access_token.encryption_key)?; - let client_state: ClientState = serde_json::from_str(&decrypted_state)?; + let decrypted_state: DecryptedString = + encrypted_state.decrypt_with_key(&access_token.encryption_key)?; + let client_state: ClientState = serde_json::from_str(decrypted_state.expose())?; if client_state.version != STATE_VERSION { return Err(Error::InvalidStateFileVersion); diff --git a/crates/bitwarden/src/tool/exporters/mod.rs b/crates/bitwarden/src/tool/exporters/mod.rs index 45bdfd3fd..07057cced 100644 --- a/crates/bitwarden/src/tool/exporters/mod.rs +++ b/crates/bitwarden/src/tool/exporters/mod.rs @@ -1,10 +1,10 @@ -use bitwarden_crypto::Decryptable; +use bitwarden_crypto::{KeyDecryptable, SensitiveString}; use bitwarden_exporters::export; use schemars::JsonSchema; use crate::{ client::{LoginMethod, UserLoginMethod}, - error::{Error, Result}, + error::{require, Error, Result}, vault::{ login::LoginUriView, Cipher, CipherType, CipherView, Collection, FieldView, Folder, FolderView, SecureNoteType, @@ -20,7 +20,7 @@ pub use client_exporter::ClientExporters; pub enum ExportFormat { Csv, Json, - EncryptedJson { password: String }, + EncryptedJson { password: SensitiveString }, } pub(super) fn export_vault( @@ -30,12 +30,13 @@ pub(super) fn export_vault( format: ExportFormat, ) -> Result { let enc = client.get_encryption_settings()?; + let key = enc.get_key(&None).ok_or(Error::VaultLocked)?; - let folders: Vec = folders.decrypt(enc, &None)?; + let folders: Vec = folders.decrypt_with_key(key)?; let folders: Vec = folders.into_iter().flat_map(|f| f.try_into()).collect(); - let ciphers: Vec = ciphers.decrypt(enc, &None)?; + let ciphers: Vec = ciphers.decrypt_with_key(key)?; let ciphers: Vec = ciphers.into_iter().flat_map(|c| c.try_into()).collect(); @@ -83,7 +84,7 @@ impl TryFrom for bitwarden_exporters::Folder { fn try_from(value: FolderView) -> Result { Ok(Self { - id: value.id.ok_or(Error::MissingFields)?, + id: require!(value.id), name: value.name, }) } @@ -95,7 +96,7 @@ impl TryFrom for bitwarden_exporters::Cipher { fn try_from(value: CipherView) -> Result { let r = match value.r#type { CipherType::Login => { - let l = value.login.ok_or(Error::MissingFields)?; + let l = require!(value.login); bitwarden_exporters::CipherType::Login(Box::new(bitwarden_exporters::Login { username: l.username, password: l.password, @@ -118,7 +119,7 @@ impl TryFrom for bitwarden_exporters::Cipher { }, )), CipherType::Card => { - let c = value.card.ok_or(Error::MissingFields)?; + let c = require!(value.card); bitwarden_exporters::CipherType::Card(Box::new(bitwarden_exporters::Card { cardholder_name: c.cardholder_name, exp_month: c.exp_month, @@ -129,7 +130,7 @@ impl TryFrom for bitwarden_exporters::Cipher { })) } CipherType::Identity => { - let i = value.identity.ok_or(Error::MissingFields)?; + let i = require!(value.identity); bitwarden_exporters::CipherType::Identity(Box::new(bitwarden_exporters::Identity { title: i.title, first_name: i.first_name, @@ -154,7 +155,7 @@ impl TryFrom for bitwarden_exporters::Cipher { }; Ok(Self { - id: value.id.ok_or(Error::MissingFields)?, + id: require!(value.id), folder_id: value.folder_id, name: value.name, notes: value.notes, @@ -206,7 +207,7 @@ impl From for bitwarden_exporters::SecureNoteType { mod tests { use std::num::NonZeroU32; - use bitwarden_crypto::Kdf; + use bitwarden_crypto::{DecryptedString, Kdf}; use chrono::{DateTime, Utc}; use super::*; @@ -216,7 +217,7 @@ mod tests { fn test_try_from_folder_view() { let view = FolderView { id: Some("fd411a1a-fec8-4070-985d-0e6560860e69".parse().unwrap()), - name: "test_name".to_string(), + name: DecryptedString::test("test_name"), revision_date: "2024-01-30T17:55:36.150Z".parse().unwrap(), }; @@ -226,7 +227,7 @@ mod tests { f.id, "fd411a1a-fec8-4070-985d-0e6560860e69".parse().unwrap() ); - assert_eq!(f.name, "test_name".to_string()); + assert_eq!(f.name, "test_name"); } #[test] @@ -234,8 +235,8 @@ mod tests { let cipher_view = CipherView { r#type: CipherType::Login, login: Some(LoginView { - username: Some("test_username".to_string()), - password: Some("test_password".to_string()), + username: Some(DecryptedString::test("test_username")), + password: Some(DecryptedString::test("test_password")), password_revision_date: None, uris: None, totp: None, @@ -247,7 +248,7 @@ mod tests { folder_id: None, collection_ids: vec![], key: None, - name: "My login".to_string(), + name: DecryptedString::test("My login"), notes: None, identity: None, card: None, @@ -273,7 +274,7 @@ mod tests { "fd411a1a-fec8-4070-985d-0e6560860e69".parse().unwrap() ); assert_eq!(cipher.folder_id, None); - assert_eq!(cipher.name, "My login".to_string()); + assert_eq!(cipher.name, "My login"); assert_eq!(cipher.notes, None); assert!(!cipher.favorite); assert_eq!(cipher.reprompt, 0); @@ -289,8 +290,8 @@ mod tests { assert_eq!(cipher.deleted_date, None); if let bitwarden_exporters::CipherType::Login(l) = cipher.r#type { - assert_eq!(l.username, Some("test_username".to_string())); - assert_eq!(l.password, Some("test_password".to_string())); + assert_eq!(l.username.unwrap(), "test_username"); + assert_eq!(l.password.unwrap(), "test_password"); assert!(l.login_uris.is_empty()); assert_eq!(l.totp, None); } else { @@ -321,7 +322,7 @@ mod tests { convert_format( &client, ExportFormat::EncryptedJson { - password: "password".to_string() + password: SensitiveString::test("password") } ) .unwrap(), diff --git a/crates/bitwarden/src/uniffi_support.rs b/crates/bitwarden/src/uniffi_support.rs index b23a9cbef..bf1eade70 100644 --- a/crates/bitwarden/src/uniffi_support.rs +++ b/crates/bitwarden/src/uniffi_support.rs @@ -1,6 +1,6 @@ use std::num::NonZeroU32; -use bitwarden_crypto::{AsymmetricEncString, EncString}; +use bitwarden_crypto::{AsymmetricEncString, EncString, SensitiveString}; use uuid::Uuid; use crate::UniffiCustomTypeConverter; @@ -12,6 +12,11 @@ uniffi::ffi_converter_forward!( bitwarden_crypto::UniFfiTag, crate::UniFfiTag ); +uniffi::ffi_converter_forward!( + SensitiveString, + bitwarden_crypto::UniFfiTag, + crate::UniFfiTag +); type DateTime = chrono::DateTime; uniffi::custom_type!(DateTime, std::time::SystemTime); diff --git a/crates/bitwarden/src/util.rs b/crates/bitwarden/src/util.rs index f6568986d..aaf47a1a6 100644 --- a/crates/bitwarden/src/util.rs +++ b/crates/bitwarden/src/util.rs @@ -1,26 +1,8 @@ -use std::num::NonZeroU32; - use base64::{ alphabet, engine::{DecodePaddingMode, GeneralPurpose, GeneralPurposeConfig}, }; -pub fn default_pbkdf2_iterations() -> NonZeroU32 { - NonZeroU32::new(600_000).unwrap() -} -#[cfg(feature = "internal")] -pub fn default_argon2_iterations() -> NonZeroU32 { - NonZeroU32::new(3).unwrap() -} -#[cfg(feature = "internal")] -pub fn default_argon2_memory() -> NonZeroU32 { - NonZeroU32::new(64).unwrap() -} -#[cfg(feature = "internal")] -pub fn default_argon2_parallelism() -> NonZeroU32 { - NonZeroU32::new(4).unwrap() -} - const INDIFFERENT: GeneralPurposeConfig = GeneralPurposeConfig::new().with_decode_padding_mode(DecodePaddingMode::Indifferent); diff --git a/crates/bitwarden/src/vault/cipher/attachment.rs b/crates/bitwarden/src/vault/cipher/attachment.rs index 1ec6be6fe..7dbec6a3f 100644 --- a/crates/bitwarden/src/vault/cipher/attachment.rs +++ b/crates/bitwarden/src/vault/cipher/attachment.rs @@ -1,5 +1,6 @@ use bitwarden_crypto::{ - CryptoError, EncString, KeyDecryptable, KeyEncryptable, SymmetricCryptoKey, + CryptoError, DecryptedString, DecryptedVec, EncString, KeyDecryptable, KeyEncryptable, + SymmetricCryptoKey, }; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; @@ -7,7 +8,7 @@ use serde::{Deserialize, Serialize}; use super::Cipher; use crate::error::{Error, Result}; -#[derive(Serialize, Deserialize, Debug, JsonSchema)] +#[derive(Serialize, Deserialize, Debug, JsonSchema, Clone)] #[serde(rename_all = "camelCase", deny_unknown_fields)] #[cfg_attr(feature = "mobile", derive(uniffi::Record))] pub struct Attachment { @@ -20,7 +21,7 @@ pub struct Attachment { pub key: Option, } -#[derive(Serialize, Deserialize, Debug, JsonSchema)] +#[derive(Serialize, Deserialize, Debug, JsonSchema, Clone)] #[serde(rename_all = "camelCase", deny_unknown_fields)] #[cfg_attr(feature = "mobile", derive(uniffi::Record))] pub struct AttachmentView { @@ -28,7 +29,7 @@ pub struct AttachmentView { pub url: Option, pub size: Option, pub size_name: Option, - pub file_name: Option, + pub file_name: Option, pub key: Option, } @@ -66,7 +67,12 @@ impl<'a> KeyEncryptable for Attachm // 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)?); + attachment.key = Some( + attachment_key + .to_vec() + .expose() + .encrypt_with_key(ciphers_key)?, + ); Ok(AttachmentEncryptResult { attachment: attachment.encrypt_with_key(ciphers_key)?, @@ -75,18 +81,18 @@ impl<'a> KeyEncryptable for Attachm } } -impl KeyDecryptable> for AttachmentFile { - fn decrypt_with_key(&self, key: &SymmetricCryptoKey) -> Result, CryptoError> { +impl KeyDecryptable for AttachmentFile { + fn decrypt_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_key: Vec = self + let attachment_key: DecryptedVec = 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())?; + let attachment_key = SymmetricCryptoKey::try_from(attachment_key)?; self.contents.decrypt_with_key(&attachment_key) } @@ -136,7 +142,7 @@ impl TryFrom for Attachment #[cfg(test)] mod tests { use base64::{engine::general_purpose::STANDARD, Engine}; - use bitwarden_crypto::{EncString, KeyDecryptable, SymmetricCryptoKey}; + use bitwarden_crypto::{EncString, KeyDecryptable, SensitiveString, SymmetricCryptoKey}; use crate::vault::{ cipher::cipher::{CipherRepromptType, CipherType}, @@ -145,7 +151,7 @@ mod tests { #[test] fn test_attachment_key() { - let user_key : SymmetricCryptoKey = "w2LO+nwV4oxwswVYCxlOfRUseXfvU03VzvKQHrqeklPgiMZrspUe6sOBToCnDn9Ay0tuCBn8ykVVRb7PWhub2Q==".parse().unwrap(); + let user_key : SymmetricCryptoKey = SensitiveString::test("w2LO+nwV4oxwswVYCxlOfRUseXfvU03VzvKQHrqeklPgiMZrspUe6sOBToCnDn9Ay0tuCBn8ykVVRb7PWhub2Q==").try_into().unwrap(); let attachment = Attachment { id: None, diff --git a/crates/bitwarden/src/vault/cipher/card.rs b/crates/bitwarden/src/vault/cipher/card.rs index cd61a17d8..3635cb2ec 100644 --- a/crates/bitwarden/src/vault/cipher/card.rs +++ b/crates/bitwarden/src/vault/cipher/card.rs @@ -1,13 +1,13 @@ use bitwarden_api_api::models::CipherCardModel; use bitwarden_crypto::{ - CryptoError, EncString, KeyDecryptable, KeyEncryptable, SymmetricCryptoKey, + CryptoError, DecryptedString, EncString, KeyDecryptable, KeyEncryptable, SymmetricCryptoKey, }; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; use crate::error::{Error, Result}; -#[derive(Serialize, Deserialize, Debug, JsonSchema)] +#[derive(Serialize, Deserialize, Debug, JsonSchema, Clone)] #[serde(rename_all = "camelCase", deny_unknown_fields)] #[cfg_attr(feature = "mobile", derive(uniffi::Record))] pub struct Card { @@ -19,16 +19,16 @@ pub struct Card { pub number: Option, } -#[derive(Serialize, Deserialize, Debug, JsonSchema)] +#[derive(Serialize, Deserialize, Debug, JsonSchema, Clone)] #[serde(rename_all = "camelCase", deny_unknown_fields)] #[cfg_attr(feature = "mobile", derive(uniffi::Record))] pub struct CardView { - pub cardholder_name: Option, - pub exp_month: Option, - pub exp_year: Option, - pub code: Option, - pub brand: Option, - pub number: Option, + pub cardholder_name: Option, + pub exp_month: Option, + pub exp_year: Option, + pub code: Option, + pub brand: Option, + pub number: Option, } impl KeyEncryptable for CardView { diff --git a/crates/bitwarden/src/vault/cipher/cipher.rs b/crates/bitwarden/src/vault/cipher/cipher.rs index f0138beb9..5e69063c0 100644 --- a/crates/bitwarden/src/vault/cipher/cipher.rs +++ b/crates/bitwarden/src/vault/cipher/cipher.rs @@ -1,7 +1,7 @@ use bitwarden_api_api::models::CipherDetailsResponseModel; use bitwarden_crypto::{ - CryptoError, EncString, KeyContainer, KeyDecryptable, KeyEncryptable, LocateKey, - SymmetricCryptoKey, + CryptoError, DecryptedString, DecryptedVec, EncString, KeyContainer, KeyDecryptable, + KeyEncryptable, LocateKey, SensitiveString, SymmetricCryptoKey, }; use chrono::{DateTime, Utc}; use schemars::JsonSchema; @@ -15,7 +15,7 @@ use super::{ login, secure_note, }; use crate::{ - error::{Error, Result}, + error::{require, Error, Result}, vault::password_history, }; @@ -37,7 +37,7 @@ pub enum CipherRepromptType { Password = 1, } -#[derive(Serialize, Deserialize, Debug, JsonSchema)] +#[derive(Serialize, Deserialize, Debug, JsonSchema, Clone)] #[serde(rename_all = "camelCase", deny_unknown_fields)] #[cfg_attr(feature = "mobile", derive(uniffi::Record))] pub struct Cipher { @@ -75,7 +75,7 @@ pub struct Cipher { pub revision_date: DateTime, } -#[derive(Serialize, Deserialize, Debug, JsonSchema)] +#[derive(Serialize, Deserialize, Debug, JsonSchema, Clone)] #[serde(rename_all = "camelCase", deny_unknown_fields)] #[cfg_attr(feature = "mobile", derive(uniffi::Record))] pub struct CipherView { @@ -86,8 +86,8 @@ pub struct CipherView { pub key: Option, - pub name: String, - pub notes: Option, + pub name: DecryptedString, + pub notes: Option, pub r#type: CipherType, pub login: Option, @@ -120,8 +120,8 @@ pub struct CipherListView { pub folder_id: Option, pub collection_ids: Vec, - pub name: String, - pub sub_title: String, + pub name: DecryptedString, + pub sub_title: DecryptedString, pub r#type: CipherType, @@ -230,77 +230,135 @@ impl Cipher { ciphers_key .as_ref() .map(|k| { - let mut key: Vec = k.decrypt_with_key(key)?; - SymmetricCryptoKey::try_from(key.as_mut_slice()) + let key: DecryptedVec = k.decrypt_with_key(key)?; + SymmetricCryptoKey::try_from(key) }) .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 { - return Ok(String::new()); + return Ok(SensitiveString::default()); }; login.username.decrypt_with_key(key)?.unwrap_or_default() } - CipherType::SecureNote => String::new(), + CipherType::SecureNote => SensitiveString::default(), CipherType::Card => { let Some(card) = &self.card else { - return Ok(String::new()); + return Ok(SensitiveString::default()); }; - let mut sub_title = String::new(); - - if let Some(brand) = &card.brand { - let brand: String = brand.decrypt_with_key(key)?; - sub_title.push_str(&brand); - } - - if let Some(number) = &card.number { - let number: String = number.decrypt_with_key(key)?; - let number_len = number.len(); - if number_len > 4 { - if !sub_title.is_empty() { - sub_title.push_str(", "); - } - - // On AMEX cards we show 5 digits instead of 4 - let digit_count = match &number[0..2] { - "34" | "37" => 5, - _ => 4, - }; - - sub_title.push_str(&number[(number_len - digit_count)..]); - } - } - - sub_title + + build_subtitle_card( + card.brand + .as_ref() + .map(|b| b.decrypt_with_key(key)) + .transpose()?, + card.number + .as_ref() + .map(|n| n.decrypt_with_key(key)) + .transpose()?, + ) } CipherType::Identity => { let Some(identity) = &self.identity else { - return Ok(String::new()); + return Ok(SensitiveString::default()); }; - let mut sub_title = String::new(); - - if let Some(first_name) = &identity.first_name { - let first_name: String = first_name.decrypt_with_key(key)?; - sub_title.push_str(&first_name); - } - - if let Some(last_name) = &identity.last_name { - if !sub_title.is_empty() { - sub_title.push(' '); - } - let last_name: String = last_name.decrypt_with_key(key)?; - sub_title.push_str(&last_name); - } - - sub_title + + build_subtitle_identity( + identity + .first_name + .as_ref() + .map(|f| f.decrypt_with_key(key)) + .transpose()?, + identity + .last_name + .as_ref() + .map(|l| l.decrypt_with_key(key)) + .transpose()?, + ) } }) } } +/// Builds the subtitle for a card cipher +/// +/// Care is taken to avoid leaking sensitive data by allocating the full size of the subtitle +fn build_subtitle_card( + brand: Option, + number: Option, +) -> SensitiveString { + let brand: Option = brand.filter(|b: &SensitiveString| !b.expose().is_empty()); + + // We only want to expose the last 4 or 5 digits of the card number + let number: Option = number + .filter(|b: &SensitiveString| b.expose().len() > 4) + .map(|n| { + // For AMEX cards show 5 digits instead of 4 + let desired_len = match &n.expose()[0..2] { + "34" | "37" => 5, + _ => 4, + }; + let start = n.expose().len() - desired_len; + + let mut str = SensitiveString::new(Box::new(String::with_capacity(desired_len + 1))); + str.expose_mut().push('*'); + str.expose_mut().push_str(&n.expose()[start..]); + + str + }); + + match (brand, number) { + (Some(brand), Some(number)) => { + let length = brand.expose().len() + 2 + number.expose().len(); + + let mut str = SensitiveString::new(Box::new(String::with_capacity(length))); + str.expose_mut().push_str(brand.expose()); + str.expose_mut().push_str(", "); + str.expose_mut().push_str(number.expose()); + + str + } + (Some(brand), None) => brand, + (None, Some(number)) => number, + _ => SensitiveString::new(Box::new("".to_owned())), + } +} + +/// Builds the subtitle for a card cipher +/// +/// Care is taken to avoid leaking sensitive data by allocating the full size of the subtitle +fn build_subtitle_identity( + first_name: Option, + last_name: Option, +) -> SensitiveString { + let first_name: Option = + first_name.filter(|f: &SensitiveString| !f.expose().is_empty()); + let last_name: Option = + last_name.filter(|l: &SensitiveString| !l.expose().is_empty()); + + match (first_name, last_name) { + (Some(first_name), Some(last_name)) => { + let length = first_name.expose().len() + 1 + last_name.expose().len(); + + let mut str = SensitiveString::new(Box::new(String::with_capacity(length))); + str.expose_mut().push_str(first_name.expose()); + str.expose_mut().push(' '); + str.expose_mut().push_str(last_name.expose()); + + str + } + (Some(first_name), None) => first_name, + (None, Some(last_name)) => last_name, + _ => SensitiveString::new(Box::new("".to_owned())), + } +} + impl CipherView { pub fn generate_cipher_key(&mut self, key: &SymmetricCryptoKey) -> Result<()> { let ciphers_key = Cipher::get_cipher_key(key, &self.key)?; @@ -308,7 +366,7 @@ impl CipherView { let new_key = SymmetricCryptoKey::generate(rand::thread_rng()); - self.key = Some(new_key.to_vec().encrypt_with_key(key)?); + self.key = Some(new_key.to_vec().expose().encrypt_with_key(key)?); Ok(()) } @@ -325,6 +383,29 @@ impl CipherView { uris.retain(|u| u.is_checksum_valid()); } } + + pub fn move_to_organization( + &mut self, + enc: &dyn KeyContainer, + organization_id: Uuid, + ) -> Result<()> { + // If the cipher has a key, we need to re-encrypt it with the new organization key + if let Some(cipher_key) = &mut self.key { + let old_key = enc + .get_key(&self.organization_id) + .ok_or(Error::VaultLocked)?; + + let new_key = enc + .get_key(&Some(organization_id)) + .ok_or(Error::VaultLocked)?; + + let dec_cipher_key: DecryptedVec = cipher_key.decrypt_with_key(old_key)?; + *cipher_key = dec_cipher_key.expose().encrypt_with_key(new_key)?; + } + + self.organization_id = Some(organization_id); + Ok(()) + } } impl KeyDecryptable for Cipher { @@ -384,9 +465,9 @@ impl TryFrom for Cipher { organization_id: cipher.organization_id, folder_id: cipher.folder_id, collection_ids: cipher.collection_ids.unwrap_or_default(), - name: EncString::try_from_optional(cipher.name)?.ok_or(Error::MissingFields)?, + name: require!(EncString::try_from_optional(cipher.name)?), notes: EncString::try_from_optional(cipher.notes)?, - r#type: cipher.r#type.ok_or(Error::MissingFields)?.into(), + r#type: require!(cipher.r#type).into(), login: cipher.login.map(|l| (*l).try_into()).transpose()?, identity: cipher.identity.map(|i| (*i).try_into()).transpose()?, card: cipher.card.map(|c| (*c).try_into()).transpose()?, @@ -412,9 +493,9 @@ impl TryFrom for Cipher { .password_history .map(|p| p.into_iter().map(|p| p.try_into()).collect()) .transpose()?, - creation_date: cipher.creation_date.ok_or(Error::MissingFields)?.parse()?, + creation_date: require!(cipher.creation_date).parse()?, deleted_date: cipher.deleted_date.map(|d| d.parse()).transpose()?, - revision_date: cipher.revision_date.ok_or(Error::MissingFields)?.parse()?, + revision_date: require!(cipher.revision_date).parse()?, key: EncString::try_from_optional(cipher.key)?, }) } @@ -443,49 +524,51 @@ impl From for CipherRepromptType #[cfg(test)] mod tests { + use std::collections::HashMap; + use super::*; + fn generate_cipher() -> CipherView { + CipherView { + r#type: CipherType::Login, + login: Some(login::LoginView { + username: Some(DecryptedString::test("test_username")), + password: Some(DecryptedString::test("test_password")), + password_revision_date: None, + uris: None, + totp: None, + autofill_on_page_load: None, + fido2_credentials: None, + }), + id: "fd411a1a-fec8-4070-985d-0e6560860e69".parse().ok(), + organization_id: None, + folder_id: None, + collection_ids: vec![], + key: None, + name: DecryptedString::test("My test login"), + notes: None, + identity: None, + card: None, + secure_note: None, + favorite: false, + reprompt: CipherRepromptType::None, + organization_use_totp: true, + edit: true, + view_password: true, + local_data: None, + attachments: None, + fields: None, + password_history: None, + creation_date: "2024-01-30T17:55:36.150Z".parse().unwrap(), + deleted_date: None, + revision_date: "2024-01-30T17:55:36.150Z".parse().unwrap(), + } + } + #[test] fn test_generate_cipher_key() { let key = SymmetricCryptoKey::generate(rand::thread_rng()); - fn generate_cipher() -> CipherView { - CipherView { - r#type: CipherType::Login, - login: Some(login::LoginView { - username: Some("test_username".to_string()), - password: Some("test_password".to_string()), - password_revision_date: None, - uris: None, - totp: None, - autofill_on_page_load: None, - fido2_credentials: None, - }), - id: "fd411a1a-fec8-4070-985d-0e6560860e69".parse().ok(), - organization_id: None, - folder_id: None, - collection_ids: vec![], - key: None, - name: "My test login".to_string(), - notes: None, - identity: None, - card: None, - secure_note: None, - favorite: false, - reprompt: CipherRepromptType::None, - organization_use_totp: true, - edit: true, - view_password: true, - local_data: None, - attachments: None, - fields: None, - password_history: None, - creation_date: "2024-01-30T17:55:36.150Z".parse().unwrap(), - deleted_date: None, - revision_date: "2024-01-30T17:55:36.150Z".parse().unwrap(), - } - } - let original_cipher = generate_cipher(); // Check that the cipher gets encrypted correctly without it's own key @@ -504,4 +587,150 @@ mod tests { assert!(key_cipher_dec.key.is_some()); assert_eq!(key_cipher_dec.name, original_cipher.name); } + + struct MockKeyContainer(HashMap, SymmetricCryptoKey>); + impl KeyContainer for MockKeyContainer { + fn get_key<'a>(&'a self, org_id: &Option) -> Option<&'a SymmetricCryptoKey> { + self.0.get(org_id) + } + } + + #[test] + fn test_move_user_cipher_to_org() { + let org = uuid::Uuid::new_v4(); + + let enc = MockKeyContainer(HashMap::from([ + (None, SymmetricCryptoKey::generate(rand::thread_rng())), + (Some(org), SymmetricCryptoKey::generate(rand::thread_rng())), + ])); + + // Create a cipher with a user key + let mut cipher = generate_cipher(); + cipher + .generate_cipher_key(enc.get_key(&None).unwrap()) + .unwrap(); + + cipher.move_to_organization(&enc, org).unwrap(); + assert_eq!(cipher.organization_id, Some(org)); + + // Check that the cipher can be encrypted/decrypted with the new org key + let org_key = enc.get_key(&Some(org)).unwrap(); + let cipher_enc = cipher.encrypt_with_key(org_key).unwrap(); + let cipher_dec: CipherView = cipher_enc.decrypt_with_key(org_key).unwrap(); + + assert_eq!(cipher_dec.name, "My test login"); + } + + #[test] + fn test_move_user_cipher_to_org_manually() { + let org = uuid::Uuid::new_v4(); + + let enc = MockKeyContainer(HashMap::from([ + (None, SymmetricCryptoKey::generate(rand::thread_rng())), + (Some(org), SymmetricCryptoKey::generate(rand::thread_rng())), + ])); + + // Create a cipher with a user key + let mut cipher = generate_cipher(); + cipher + .generate_cipher_key(enc.get_key(&None).unwrap()) + .unwrap(); + + cipher.organization_id = Some(org); + + // Check that the cipher can not be encrypted, as the + // cipher key is tied to the user key and not the org key + let org_key = enc.get_key(&Some(org)).unwrap(); + assert!(cipher.encrypt_with_key(org_key).is_err()); + } + + #[test] + fn test_build_subtitle_card_visa() { + let brand = Some(DecryptedString::test("Visa")); + let number = Some(DecryptedString::test("4111111111111111")); + + let subtitle = build_subtitle_card(brand, number); + assert_eq!(subtitle, "Visa, *1111"); + } + + #[test] + fn test_build_subtitle_card_mastercard() { + let brand = Some(DecryptedString::test("Mastercard")); + let number = Some(DecryptedString::test("5555555555554444")); + + let subtitle = build_subtitle_card(brand, number); + assert_eq!(subtitle, "Mastercard, *4444"); + } + + #[test] + fn test_build_subtitle_card_amex() { + let brand = Some(DecryptedString::test("Amex")); + let number = Some(DecryptedString::test("378282246310005")); + + let subtitle = build_subtitle_card(brand, number); + assert_eq!(subtitle, "Amex, *10005"); + } + + #[test] + fn test_build_subtitle_card_underflow() { + let brand = Some(DecryptedString::test("Mastercard")); + let number = Some(DecryptedString::test("4")); + + let subtitle = build_subtitle_card(brand, number); + assert_eq!(subtitle, "Mastercard"); + } + + #[test] + fn test_build_subtitle_card_only_brand() { + let brand = Some(DecryptedString::test("Mastercard")); + let number = None; + + let subtitle = build_subtitle_card(brand, number); + assert_eq!(subtitle, "Mastercard"); + } + + #[test] + fn test_build_subtitle_card_only_card() { + let brand = None; + let number = Some(DecryptedString::test("5555555555554444")); + + let subtitle = build_subtitle_card(brand, number); + assert_eq!(subtitle, "*4444"); + } + + #[test] + fn test_build_subtitle_identity() { + let first_name = Some(DecryptedString::test("John")); + let last_name = Some(DecryptedString::test("Doe")); + + let subtitle = build_subtitle_identity(first_name, last_name); + assert_eq!(subtitle, "John Doe"); + } + + #[test] + fn test_build_subtitle_identity_only_first() { + let first_name = Some(DecryptedString::test("John")); + let last_name = None; + + let subtitle = build_subtitle_identity(first_name, last_name); + assert_eq!(subtitle, "John"); + } + + #[test] + fn test_build_subtitle_identity_only_last() { + let first_name = None; + let last_name = Some(DecryptedString::test("Doe")); + + let subtitle = build_subtitle_identity(first_name, last_name); + assert_eq!(subtitle, "Doe"); + } + + #[test] + fn test_build_subtitle_identity_none() { + let first_name = None; + let last_name = None; + + let subtitle = build_subtitle_identity(first_name, last_name); + assert_eq!(subtitle, ""); + } } diff --git a/crates/bitwarden/src/vault/cipher/field.rs b/crates/bitwarden/src/vault/cipher/field.rs index 25a318d6f..04837bf9e 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, + CryptoError, DecryptedString, 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::error::{Error, Result}; +use crate::error::{require, Error, Result}; #[derive(Clone, Copy, Serialize_repr, Deserialize_repr, Debug, JsonSchema)] #[repr(u8)] @@ -19,7 +19,7 @@ pub enum FieldType { Linked = 3, } -#[derive(Serialize, Deserialize, Debug, JsonSchema)] +#[derive(Serialize, Deserialize, Debug, JsonSchema, Clone)] #[serde(rename_all = "camelCase", deny_unknown_fields)] #[cfg_attr(feature = "mobile", derive(uniffi::Record))] pub struct Field { @@ -30,12 +30,12 @@ pub struct Field { linked_id: Option, } -#[derive(Serialize, Deserialize, Debug, JsonSchema)] +#[derive(Serialize, Deserialize, Debug, JsonSchema, Clone)] #[serde(rename_all = "camelCase", deny_unknown_fields)] #[cfg_attr(feature = "mobile", derive(uniffi::Record))] pub struct FieldView { - pub(crate) name: Option, - pub(crate) value: Option, + pub(crate) name: Option, + pub(crate) value: Option, pub(crate) r#type: FieldType, pub(crate) linked_id: Option, @@ -70,7 +70,7 @@ impl TryFrom for Field { Ok(Self { name: EncString::try_from_optional(model.name)?, value: EncString::try_from_optional(model.value)?, - r#type: model.r#type.map(|t| t.into()).ok_or(Error::MissingFields)?, + r#type: require!(model.r#type).into(), linked_id: model .linked_id .map(|id| (id as u32).try_into()) diff --git a/crates/bitwarden/src/vault/cipher/identity.rs b/crates/bitwarden/src/vault/cipher/identity.rs index f59166eec..f131fd597 100644 --- a/crates/bitwarden/src/vault/cipher/identity.rs +++ b/crates/bitwarden/src/vault/cipher/identity.rs @@ -1,13 +1,13 @@ use bitwarden_api_api::models::CipherIdentityModel; use bitwarden_crypto::{ - CryptoError, EncString, KeyDecryptable, KeyEncryptable, SymmetricCryptoKey, + CryptoError, DecryptedString, EncString, KeyDecryptable, KeyEncryptable, SymmetricCryptoKey, }; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; use crate::error::{Error, Result}; -#[derive(Serialize, Deserialize, Debug, JsonSchema)] +#[derive(Serialize, Deserialize, Debug, JsonSchema, Clone)] #[serde(rename_all = "camelCase", deny_unknown_fields)] #[cfg_attr(feature = "mobile", derive(uniffi::Record))] pub struct Identity { @@ -31,28 +31,28 @@ pub struct Identity { pub license_number: Option, } -#[derive(Serialize, Deserialize, Debug, JsonSchema)] +#[derive(Serialize, Deserialize, Debug, JsonSchema, Clone)] #[serde(rename_all = "camelCase", deny_unknown_fields)] #[cfg_attr(feature = "mobile", derive(uniffi::Record))] pub struct IdentityView { - pub title: Option, - pub first_name: Option, - pub middle_name: Option, - pub last_name: Option, - pub address1: Option, - pub address2: Option, - pub address3: Option, - pub city: Option, - pub state: Option, - pub postal_code: Option, - pub country: Option, - pub company: Option, - pub email: Option, - pub phone: Option, - pub ssn: Option, - pub username: Option, - pub passport_number: Option, - pub license_number: Option, + pub title: Option, + pub first_name: Option, + pub middle_name: Option, + pub last_name: Option, + pub address1: Option, + pub address2: Option, + pub address3: Option, + pub city: Option, + pub state: Option, + pub postal_code: Option, + pub country: Option, + pub company: Option, + pub email: Option, + pub phone: Option, + pub ssn: Option, + pub username: Option, + pub passport_number: Option, + pub license_number: Option, } impl KeyEncryptable for IdentityView { diff --git a/crates/bitwarden/src/vault/cipher/linked_id.rs b/crates/bitwarden/src/vault/cipher/linked_id.rs index 77429438e..b52d756c6 100644 --- a/crates/bitwarden/src/vault/cipher/linked_id.rs +++ b/crates/bitwarden/src/vault/cipher/linked_id.rs @@ -112,7 +112,7 @@ impl TryFrom for LinkedIdType { 416 => Ok(LinkedIdType::Identity(IdentityLinkedIdType::FirstName)), 417 => Ok(LinkedIdType::Identity(IdentityLinkedIdType::LastName)), 418 => Ok(LinkedIdType::Identity(IdentityLinkedIdType::FullName)), - _ => Err(Error::MissingFields), + _ => Err(Error::MissingFields("LinkedIdType")), } } } diff --git a/crates/bitwarden/src/vault/cipher/local_data.rs b/crates/bitwarden/src/vault/cipher/local_data.rs index 6a85512c2..fd5897066 100644 --- a/crates/bitwarden/src/vault/cipher/local_data.rs +++ b/crates/bitwarden/src/vault/cipher/local_data.rs @@ -2,7 +2,7 @@ use bitwarden_crypto::{CryptoError, KeyDecryptable, KeyEncryptable, SymmetricCry use schemars::JsonSchema; use serde::{Deserialize, Serialize}; -#[derive(Serialize, Deserialize, Debug, JsonSchema)] +#[derive(Serialize, Deserialize, Debug, JsonSchema, Clone)] #[serde(rename_all = "camelCase", deny_unknown_fields)] #[cfg_attr(feature = "mobile", derive(uniffi::Record))] pub struct LocalData { @@ -10,7 +10,7 @@ pub struct LocalData { last_launched: Option, } -#[derive(Serialize, Deserialize, Debug, JsonSchema)] +#[derive(Serialize, Deserialize, Debug, JsonSchema, Clone)] #[serde(rename_all = "camelCase", deny_unknown_fields)] #[cfg_attr(feature = "mobile", derive(uniffi::Record))] pub struct LocalDataView { diff --git a/crates/bitwarden/src/vault/cipher/login.rs b/crates/bitwarden/src/vault/cipher/login.rs index e5731dbee..fd4e291bf 100644 --- a/crates/bitwarden/src/vault/cipher/login.rs +++ b/crates/bitwarden/src/vault/cipher/login.rs @@ -1,14 +1,17 @@ -use base64::{engine::general_purpose::STANDARD, Engine}; +use base64::engine::general_purpose::STANDARD; use bitwarden_api_api::models::{CipherLoginModel, CipherLoginUriModel}; use bitwarden_crypto::{ - CryptoError, EncString, KeyDecryptable, KeyEncryptable, SymmetricCryptoKey, + CryptoError, DecryptedString, EncString, KeyDecryptable, KeyEncryptable, Sensitive, + SensitiveString, SensitiveVec, SymmetricCryptoKey, }; use chrono::{DateTime, Utc}; +use hmac::digest::generic_array::GenericArray; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; use serde_repr::{Deserialize_repr, Serialize_repr}; +use sha2::Digest; -use crate::error::{Error, Result}; +use crate::error::{require, Error, Result}; #[derive(Clone, Copy, Serialize_repr, Deserialize_repr, Debug, JsonSchema)] #[repr(u8)] @@ -23,7 +26,7 @@ pub enum UriMatchType { Never = 5, } -#[derive(Serialize, Deserialize, Debug, JsonSchema)] +#[derive(Serialize, Deserialize, Debug, JsonSchema, Clone)] #[serde(rename_all = "camelCase", deny_unknown_fields)] #[cfg_attr(feature = "mobile", derive(uniffi::Record))] pub struct LoginUri { @@ -32,13 +35,13 @@ pub struct LoginUri { pub uri_checksum: Option, } -#[derive(Serialize, Deserialize, Debug, JsonSchema)] +#[derive(Serialize, Deserialize, Debug, JsonSchema, Clone)] #[serde(rename_all = "camelCase", deny_unknown_fields)] #[cfg_attr(feature = "mobile", derive(uniffi::Record))] pub struct LoginUriView { - pub uri: Option, + pub uri: Option, pub r#match: Option, - pub uri_checksum: Option, + pub uri_checksum: Option, } impl LoginUriView { @@ -49,22 +52,28 @@ impl LoginUriView { let Some(cs) = &self.uri_checksum else { return false; }; - let Ok(cs) = STANDARD.decode(cs) else { + let Ok(cs) = cs.clone().decode_base64(STANDARD) else { return false; }; - use sha2::Digest; - let uri_hash = sha2::Sha256::new().chain_update(uri.as_bytes()).finalize(); + let uri_hash: Sensitive> = Sensitive::new(Box::new( + sha2::Sha256::new() + .chain_update(uri.expose().as_bytes()) + .finalize(), + )); - uri_hash.as_slice() == cs + cs == uri_hash.expose().as_slice() } pub(crate) fn generate_checksum(&mut self) { if let Some(uri) = &self.uri { - use sha2::Digest; - let uri_hash = sha2::Sha256::new().chain_update(uri.as_bytes()).finalize(); - let uri_hash = STANDARD.encode(uri_hash.as_slice()); - self.uri_checksum = Some(uri_hash); + let uri_hash: SensitiveVec = Sensitive::new(Box::new( + sha2::Sha256::new() + .chain_update(uri.expose().as_bytes()) + .finalize(), + )) + .into(); + self.uri_checksum = Some(uri_hash.encode_base64(STANDARD)) } } } @@ -88,7 +97,26 @@ pub struct Fido2Credential { pub creation_date: DateTime, } -#[derive(Serialize, Deserialize, Debug, JsonSchema)] +#[derive(Serialize, Deserialize, Debug, JsonSchema, Clone)] +#[serde(rename_all = "camelCase", deny_unknown_fields)] +#[cfg_attr(feature = "mobile", derive(uniffi::Record))] +pub struct Fido2CredentialView { + pub credential_id: SensitiveString, + pub key_type: SensitiveString, + pub key_algorithm: SensitiveString, + pub key_curve: SensitiveString, + pub key_value: SensitiveString, + pub rp_id: SensitiveString, + pub user_handle: Option, + pub user_name: Option, + pub counter: SensitiveString, + pub rp_name: Option, + pub user_display_name: Option, + pub discoverable: SensitiveString, + pub creation_date: DateTime, +} + +#[derive(Serialize, Deserialize, Debug, JsonSchema, Clone)] #[serde(rename_all = "camelCase", deny_unknown_fields)] #[cfg_attr(feature = "mobile", derive(uniffi::Record))] pub struct Login { @@ -103,16 +131,16 @@ pub struct Login { pub fido2_credentials: Option>, } -#[derive(Serialize, Deserialize, Debug, JsonSchema)] +#[derive(Serialize, Deserialize, Debug, JsonSchema, Clone)] #[serde(rename_all = "camelCase", deny_unknown_fields)] #[cfg_attr(feature = "mobile", derive(uniffi::Record))] pub struct LoginView { - pub username: Option, - pub password: Option, + pub username: Option, + pub password: Option, pub password_revision_date: Option>, pub uris: Option>, - pub totp: Option, + pub totp: Option, pub autofill_on_page_load: Option, // TODO: Remove this once the SDK supports state @@ -167,6 +195,49 @@ impl KeyDecryptable for Login { } } +impl KeyEncryptable for Fido2CredentialView { + fn encrypt_with_key(self, key: &SymmetricCryptoKey) -> Result { + Ok(Fido2Credential { + credential_id: self.credential_id.encrypt_with_key(key)?, + key_type: self.key_type.encrypt_with_key(key)?, + key_algorithm: self.key_algorithm.encrypt_with_key(key)?, + key_curve: self.key_curve.encrypt_with_key(key)?, + key_value: self.key_value.encrypt_with_key(key)?, + rp_id: self.rp_id.encrypt_with_key(key)?, + user_handle: self.user_handle.encrypt_with_key(key)?, + user_name: self.user_name.encrypt_with_key(key)?, + counter: self.counter.encrypt_with_key(key)?, + rp_name: self.rp_name.encrypt_with_key(key)?, + user_display_name: self.user_display_name.encrypt_with_key(key)?, + discoverable: self.discoverable.encrypt_with_key(key)?, + creation_date: self.creation_date, + }) + } +} + +impl KeyDecryptable for Fido2Credential { + fn decrypt_with_key( + &self, + key: &SymmetricCryptoKey, + ) -> Result { + Ok(Fido2CredentialView { + credential_id: self.credential_id.decrypt_with_key(key)?, + key_type: self.key_type.decrypt_with_key(key)?, + key_algorithm: self.key_algorithm.decrypt_with_key(key)?, + key_curve: self.key_curve.decrypt_with_key(key)?, + key_value: self.key_value.decrypt_with_key(key)?, + rp_id: self.rp_id.decrypt_with_key(key)?, + user_handle: self.user_handle.decrypt_with_key(key)?, + user_name: self.user_name.decrypt_with_key(key)?, + counter: self.counter.decrypt_with_key(key)?, + rp_name: self.rp_name.decrypt_with_key(key)?, + user_display_name: self.user_display_name.decrypt_with_key(key)?, + discoverable: self.discoverable.decrypt_with_key(key)?, + creation_date: self.creation_date, + }) + } +} + impl TryFrom for Login { type Error = Error; @@ -222,22 +293,22 @@ impl TryFrom for Fido2Cre fn try_from(value: bitwarden_api_api::models::CipherFido2CredentialModel) -> Result { Ok(Self { - credential_id: value.credential_id.ok_or(Error::MissingFields)?.parse()?, - key_type: value.key_type.ok_or(Error::MissingFields)?.parse()?, - key_algorithm: value.key_algorithm.ok_or(Error::MissingFields)?.parse()?, - key_curve: value.key_curve.ok_or(Error::MissingFields)?.parse()?, - key_value: value.key_value.ok_or(Error::MissingFields)?.parse()?, - rp_id: value.rp_id.ok_or(Error::MissingFields)?.parse()?, + credential_id: require!(value.credential_id).parse()?, + key_type: require!(value.key_type).parse()?, + key_algorithm: require!(value.key_algorithm).parse()?, + key_curve: require!(value.key_curve).parse()?, + key_value: require!(value.key_value).parse()?, + rp_id: require!(value.rp_id).parse()?, user_handle: EncString::try_from_optional(value.user_handle) .ok() .flatten(), user_name: EncString::try_from_optional(value.user_name).ok().flatten(), - counter: value.counter.ok_or(Error::MissingFields)?.parse()?, + counter: require!(value.counter).parse()?, rp_name: EncString::try_from_optional(value.rp_name).ok().flatten(), user_display_name: EncString::try_from_optional(value.user_display_name) .ok() .flatten(), - discoverable: value.discoverable.ok_or(Error::MissingFields)?.parse()?, + discoverable: require!(value.discoverable).parse()?, creation_date: value.creation_date.parse()?, }) } @@ -245,12 +316,16 @@ impl TryFrom for Fido2Cre #[cfg(test)] mod tests { + use bitwarden_crypto::SensitiveString; + #[test] fn test_valid_checksum() { let uri = super::LoginUriView { - uri: Some("https://example.com".to_string()), + uri: Some(SensitiveString::test("https://example.com")), r#match: Some(super::UriMatchType::Domain), - uri_checksum: Some("EAaArVRs5qV39C9S3zO0z9ynVoWeZkuNfeMpsVDQnOk=".to_string()), + uri_checksum: Some(SensitiveString::test( + "EAaArVRs5qV39C9S3zO0z9ynVoWeZkuNfeMpsVDQnOk=", + )), }; assert!(uri.is_checksum_valid()); } @@ -258,9 +333,11 @@ mod tests { #[test] fn test_invalid_checksum() { let uri = super::LoginUriView { - uri: Some("https://example.com".to_string()), + uri: Some(SensitiveString::test("https://example.com")), r#match: Some(super::UriMatchType::Domain), - uri_checksum: Some("UtSgIv8LYfEdOu7yqjF7qXWhmouYGYC8RSr7/ryZg5Q=".to_string()), + uri_checksum: Some(SensitiveString::test( + "UtSgIv8LYfEdOu7yqjF7qXWhmouYGYC8RSr7/ryZg5Q=", + )), }; assert!(!uri.is_checksum_valid()); } @@ -268,7 +345,7 @@ mod tests { #[test] fn test_missing_checksum() { let uri = super::LoginUriView { - uri: Some("https://example.com".to_string()), + uri: Some(SensitiveString::test("https://example.com")), r#match: Some(super::UriMatchType::Domain), uri_checksum: None, }; @@ -278,7 +355,7 @@ mod tests { #[test] fn test_generate_checksum() { let mut uri = super::LoginUriView { - uri: Some("https://test.com".to_string()), + uri: Some(SensitiveString::test("https://test.com")), r#match: Some(super::UriMatchType::Domain), uri_checksum: None, }; @@ -286,7 +363,7 @@ mod tests { uri.generate_checksum(); assert_eq!( - uri.uri_checksum.unwrap().as_str(), + uri.uri_checksum.unwrap().expose(), "OWk2vQvwYD1nhLZdA+ltrpBWbDa2JmHyjUEWxRZSS8w=" ); } diff --git a/crates/bitwarden/src/vault/cipher/mod.rs b/crates/bitwarden/src/vault/cipher/mod.rs index c2b49eb37..89f1bc911 100644 --- a/crates/bitwarden/src/vault/cipher/mod.rs +++ b/crates/bitwarden/src/vault/cipher/mod.rs @@ -14,4 +14,5 @@ pub use attachment::{ }; pub use cipher::{Cipher, CipherListView, CipherRepromptType, CipherType, CipherView}; pub use field::FieldView; +pub use login::{Fido2Credential, Fido2CredentialView}; pub use secure_note::SecureNoteType; diff --git a/crates/bitwarden/src/vault/cipher/secure_note.rs b/crates/bitwarden/src/vault/cipher/secure_note.rs index eba4ea2fb..ebcbc3fa4 100644 --- a/crates/bitwarden/src/vault/cipher/secure_note.rs +++ b/crates/bitwarden/src/vault/cipher/secure_note.rs @@ -4,7 +4,7 @@ use schemars::JsonSchema; use serde::{Deserialize, Serialize}; use serde_repr::{Deserialize_repr, Serialize_repr}; -use crate::error::{Error, Result}; +use crate::error::{require, Error, Result}; #[derive(Clone, Copy, Serialize_repr, Deserialize_repr, Debug, JsonSchema)] #[repr(u8)] @@ -48,7 +48,7 @@ impl TryFrom for SecureNote { fn try_from(model: CipherSecureNoteModel) -> Result { Ok(Self { - r#type: model.r#type.map(|t| t.into()).ok_or(Error::MissingFields)?, + r#type: require!(model.r#type).into(), }) } } diff --git a/crates/bitwarden/src/vault/collection.rs b/crates/bitwarden/src/vault/collection.rs index d20e5d729..eed60aa1c 100644 --- a/crates/bitwarden/src/vault/collection.rs +++ b/crates/bitwarden/src/vault/collection.rs @@ -1,12 +1,13 @@ use bitwarden_api_api::models::CollectionDetailsResponseModel; use bitwarden_crypto::{ - CryptoError, EncString, KeyContainer, KeyDecryptable, LocateKey, SymmetricCryptoKey, + CryptoError, DecryptedString, EncString, KeyContainer, KeyDecryptable, LocateKey, + SymmetricCryptoKey, }; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; use uuid::Uuid; -use crate::error::{Error, Result}; +use crate::error::{require, Error, Result}; #[derive(Serialize, Deserialize, Debug, JsonSchema)] #[serde(rename_all = "camelCase", deny_unknown_fields)] @@ -29,7 +30,7 @@ pub struct CollectionView { id: Option, organization_id: Uuid, - name: String, + name: DecryptedString, external_id: Option, hide_passwords: bool, @@ -66,8 +67,8 @@ impl TryFrom for Collection { fn try_from(collection: CollectionDetailsResponseModel) -> Result { Ok(Collection { id: collection.id, - organization_id: collection.organization_id.ok_or(Error::MissingFields)?, - name: collection.name.ok_or(Error::MissingFields)?.parse()?, + organization_id: require!(collection.organization_id), + name: require!(collection.name).parse()?, external_id: collection.external_id, hide_passwords: collection.hide_passwords.unwrap_or(false), read_only: collection.read_only.unwrap_or(false), diff --git a/crates/bitwarden/src/vault/folder.rs b/crates/bitwarden/src/vault/folder.rs index edd1cac42..315abeca3 100644 --- a/crates/bitwarden/src/vault/folder.rs +++ b/crates/bitwarden/src/vault/folder.rs @@ -1,13 +1,14 @@ use bitwarden_api_api::models::FolderResponseModel; use bitwarden_crypto::{ - CryptoError, EncString, KeyDecryptable, KeyEncryptable, LocateKey, SymmetricCryptoKey, + CryptoError, DecryptedString, EncString, KeyDecryptable, KeyEncryptable, LocateKey, + SymmetricCryptoKey, }; use chrono::{DateTime, Utc}; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; use uuid::Uuid; -use crate::error::{Error, Result}; +use crate::error::{require, Error, Result}; #[derive(Serialize, Deserialize, Debug, JsonSchema)] #[serde(rename_all = "camelCase")] @@ -23,7 +24,7 @@ pub struct Folder { #[cfg_attr(feature = "mobile", derive(uniffi::Record))] pub struct FolderView { pub id: Option, - pub name: String, + pub name: DecryptedString, pub revision_date: DateTime, } @@ -55,8 +56,8 @@ impl TryFrom for Folder { fn try_from(folder: FolderResponseModel) -> Result { Ok(Folder { id: folder.id, - name: EncString::try_from_optional(folder.name)?.ok_or(Error::MissingFields)?, - revision_date: folder.revision_date.ok_or(Error::MissingFields)?.parse()?, + name: require!(EncString::try_from_optional(folder.name)?), + revision_date: require!(folder.revision_date).parse()?, }) } } diff --git a/crates/bitwarden/src/vault/password_history.rs b/crates/bitwarden/src/vault/password_history.rs index 2ec20116b..f862bf2c5 100644 --- a/crates/bitwarden/src/vault/password_history.rs +++ b/crates/bitwarden/src/vault/password_history.rs @@ -1,6 +1,7 @@ use bitwarden_api_api::models::CipherPasswordHistoryModel; use bitwarden_crypto::{ - CryptoError, EncString, KeyDecryptable, KeyEncryptable, LocateKey, SymmetricCryptoKey, + CryptoError, DecryptedString, EncString, KeyDecryptable, KeyEncryptable, LocateKey, + SymmetricCryptoKey, }; use chrono::{DateTime, Utc}; use schemars::JsonSchema; @@ -8,7 +9,7 @@ use serde::{Deserialize, Serialize}; use crate::error::{Error, Result}; -#[derive(Serialize, Deserialize, Debug, JsonSchema)] +#[derive(Serialize, Deserialize, Debug, JsonSchema, Clone)] #[serde(rename_all = "camelCase", deny_unknown_fields)] #[cfg_attr(feature = "mobile", derive(uniffi::Record))] pub struct PasswordHistory { @@ -16,11 +17,11 @@ pub struct PasswordHistory { last_used_date: DateTime, } -#[derive(Serialize, Deserialize, Debug, JsonSchema)] +#[derive(Serialize, Deserialize, Debug, JsonSchema, Clone)] #[serde(rename_all = "camelCase", deny_unknown_fields)] #[cfg_attr(feature = "mobile", derive(uniffi::Record))] pub struct PasswordHistoryView { - password: String, + password: DecryptedString, last_used_date: DateTime, } diff --git a/crates/bitwarden/src/vault/send.rs b/crates/bitwarden/src/vault/send.rs index 144933899..dba36e44a 100644 --- a/crates/bitwarden/src/vault/send.rs +++ b/crates/bitwarden/src/vault/send.rs @@ -1,11 +1,9 @@ -use base64::{ - engine::general_purpose::{STANDARD, URL_SAFE_NO_PAD}, - Engine, -}; +use base64::engine::general_purpose::{STANDARD, URL_SAFE_NO_PAD}; use bitwarden_api_api::models::{SendFileModel, SendResponseModel, SendTextModel}; use bitwarden_crypto::{ - derive_shareable_key, generate_random_bytes, CryptoError, EncString, KeyDecryptable, - KeyEncryptable, LocateKey, SymmetricCryptoKey, + derive_shareable_key, generate_random_bytes, CryptoError, DecryptedString, DecryptedVec, + EncString, KeyDecryptable, KeyEncryptable, LocateKey, Sensitive, SensitiveString, SensitiveVec, + SymmetricCryptoKey, }; use chrono::{DateTime, Utc}; use schemars::JsonSchema; @@ -13,7 +11,7 @@ use serde::{Deserialize, Serialize}; use serde_repr::{Deserialize_repr, Serialize_repr}; use uuid::Uuid; -use crate::error::{Error, Result}; +use crate::error::{require, Error, Result}; const SEND_ITERATIONS: u32 = 100_000; @@ -33,7 +31,7 @@ pub struct SendFile { #[cfg_attr(feature = "mobile", derive(uniffi::Record))] pub struct SendFileView { pub id: Option, - pub file_name: String, + pub file_name: DecryptedString, pub size: Option, /// Readable size, ex: "4.2 KB" or "1.43 GB" pub size_name: Option, @@ -51,7 +49,7 @@ pub struct SendText { #[serde(rename_all = "camelCase", deny_unknown_fields)] #[cfg_attr(feature = "mobile", derive(uniffi::Record))] pub struct SendTextView { - pub text: Option, + pub text: Option, pub hidden: bool, } @@ -73,7 +71,7 @@ pub struct Send { pub name: EncString, pub notes: Option, pub key: EncString, - pub password: Option, + pub password: Option, pub r#type: SendType, pub file: Option, @@ -96,10 +94,10 @@ pub struct SendView { pub id: Option, pub access_id: Option, - pub name: String, - pub notes: Option, + pub name: DecryptedString, + pub notes: Option, /// Base64 encoded key - pub key: Option, + pub key: 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. @@ -122,14 +120,14 @@ pub struct SendView { pub expiration_date: Option>, } -#[derive(Serialize, Deserialize, Debug, JsonSchema)] +#[derive(Serialize, Deserialize, JsonSchema)] #[serde(rename_all = "camelCase", deny_unknown_fields)] #[cfg_attr(feature = "mobile", derive(uniffi::Record))] pub struct SendListView { pub id: Option, pub access_id: Option, - pub name: String, + pub name: DecryptedString, pub r#type: SendType, pub disabled: bool, @@ -144,12 +142,12 @@ impl Send { send_key: &EncString, enc_key: &SymmetricCryptoKey, ) -> Result { - let key: Vec = send_key.decrypt_with_key(enc_key)?; + let key: DecryptedVec = send_key.decrypt_with_key(enc_key)?; Self::derive_shareable_key(&key) } - fn derive_shareable_key(key: &[u8]) -> Result { - let key = key.try_into().map_err(|_| CryptoError::InvalidKeyLen)?; + fn derive_shareable_key(key: &SensitiveVec) -> Result { + let key = key.try_into()?; Ok(derive_shareable_key(key, "send", Some("send"))) } } @@ -200,7 +198,7 @@ impl KeyDecryptable for Send { // 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 k: DecryptedVec = self.key.decrypt_with_key(key)?; let key = Send::derive_shareable_key(&k)?; Ok(SendView { @@ -209,7 +207,7 @@ impl KeyDecryptable for Send { 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)), + key: Some(k.encode_base64(URL_SAFE_NO_PAD)), new_password: None, has_password: self.password.is_some(), @@ -260,13 +258,13 @@ impl KeyEncryptable for SendView { // the stretched key let k = match (self.key, self.id) { // Existing send, decrypt key - (Some(k), _) => URL_SAFE_NO_PAD - .decode(k) + (Some(k), _) => k + .decode_base64(URL_SAFE_NO_PAD) .map_err(|_| CryptoError::InvalidKey)?, // New send, generate random key (None, None) => { - let key: [u8; 16] = generate_random_bytes(); - key.to_vec() + let key: Sensitive<[u8; 16]> = generate_random_bytes(); + key.into() } // Existing send without key _ => return Err(CryptoError::InvalidKey), @@ -279,10 +277,11 @@ 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)?, + key: k.expose().encrypt_with_key(key)?, password: self.new_password.map(|password| { - let password = bitwarden_crypto::pbkdf2(password.as_bytes(), &k, SEND_ITERATIONS); - STANDARD.encode(password) + let password = + bitwarden_crypto::pbkdf2(password.as_bytes(), k.expose(), SEND_ITERATIONS); + password.encode_base64(STANDARD) }), r#type: self.r#type, @@ -308,19 +307,19 @@ impl TryFrom for Send { Ok(Send { id: send.id, access_id: send.access_id, - name: send.name.ok_or(Error::MissingFields)?.parse()?, + name: require!(send.name).parse()?, notes: EncString::try_from_optional(send.notes)?, - key: send.key.ok_or(Error::MissingFields)?.parse()?, - password: send.password, - r#type: send.r#type.ok_or(Error::MissingFields)?.into(), + key: require!(send.key).parse()?, + password: send.password.map(|p| SensitiveString::new(Box::new(p))), + r#type: require!(send.r#type).into(), file: send.file.map(|f| (*f).try_into()).transpose()?, text: send.text.map(|t| (*t).try_into()).transpose()?, max_access_count: send.max_access_count.map(|s| s as u32), - access_count: send.access_count.ok_or(Error::MissingFields)? as u32, + access_count: require!(send.access_count) as u32, disabled: send.disabled.unwrap_or(false), hide_email: send.hide_email.unwrap_or(false), - revision_date: send.revision_date.ok_or(Error::MissingFields)?.parse()?, - deletion_date: send.deletion_date.ok_or(Error::MissingFields)?.parse()?, + revision_date: require!(send.revision_date).parse()?, + deletion_date: require!(send.deletion_date).parse()?, expiration_date: send.expiration_date.map(|s| s.parse()).transpose()?, }) } @@ -341,7 +340,7 @@ impl TryFrom for SendFile { fn try_from(file: SendFileModel) -> Result { Ok(SendFile { id: file.id, - file_name: file.file_name.ok_or(Error::MissingFields)?.parse()?, + file_name: require!(file.file_name).parse()?, size: file.size.map(|v| v.to_string()), size_name: file.size_name, }) @@ -361,26 +360,29 @@ impl TryFrom for SendText { #[cfg(test)] mod tests { - use bitwarden_crypto::{KeyDecryptable, KeyEncryptable}; + use bitwarden_crypto::{ + KeyDecryptable, KeyEncryptable, MasterKey, SensitiveString, SensitiveVec, + }; use super::{Send, SendText, SendTextView, SendType}; use crate::{ - client::{encryption_settings::EncryptionSettings, Kdf, UserLoginMethod}, + client::{encryption_settings::EncryptionSettings, Kdf}, vault::SendView, }; #[test] fn test_get_send_key() { // Initialize user encryption with some test data - let enc = EncryptionSettings::new( - &UserLoginMethod::Username { - client_id: "test".into(), - email: "test@bitwarden.com".into(), - kdf: Kdf::PBKDF2 { - iterations: 345123.try_into().unwrap(), - }, + let master_key = MasterKey::derive( + &SensitiveVec::test(b"asdfasdfasdf"), + "test@bitwarden.com".as_bytes(), + &Kdf::PBKDF2 { + iterations: 345123.try_into().unwrap(), }, - "asdfasdfasdf", + ) + .unwrap(); + let enc = EncryptionSettings::new( + master_key, "2.majkL1/hNz9yptLqNAUSnw==|RiOzMTTJMG948qu8O3Zm1EQUO2E8BuTwFKnO9LWQjMzxMWJM5GbyOq2/A+tumPbTERt4JWur/FKfgHb+gXuYiEYlXPMuVBvT7nv4LPytJuM=|IVqMxHJeR1ZXY0sGngTC0x+WqbG8p6V+BTrdgBbQXjM=".parse().unwrap(), "2.kmLY8NJVuiKBFJtNd/ZFpA==|qOodlRXER+9ogCe3yOibRHmUcSNvjSKhdDuztLlucs10jLiNoVVVAc+9KfNErLSpx5wmUF1hBOJM8zwVPjgQTrmnNf/wuDpwiaCxNYb/0v4FygPy7ccAHK94xP1lfqq7U9+tv+/yiZSwgcT+xF0wFpoxQeNdNRFzPTuD9o4134n8bzacD9DV/WjcrXfRjbBCzzuUGj1e78+A7BWN7/5IWLz87KWk8G7O/W4+8PtEzlwkru6Wd1xO19GYU18oArCWCNoegSmcGn7w7NDEXlwD403oY8Oa7ylnbqGE28PVJx+HLPNIdSC6YKXeIOMnVs7Mctd/wXC93zGxAWD6ooTCzHSPVV50zKJmWIG2cVVUS7j35H3rGDtUHLI+ASXMEux9REZB8CdVOZMzp2wYeiOpggebJy6MKOZqPT1R3X0fqF2dHtRFPXrNsVr1Qt6bS9qTyO4ag1/BCvXF3P1uJEsI812BFAne3cYHy5bIOxuozPfipJrTb5WH35bxhElqwT3y/o/6JWOGg3HLDun31YmiZ2HScAsUAcEkA4hhoTNnqy4O2s3yVbCcR7jF7NLsbQc0MDTbnjxTdI4VnqUIn8s2c9hIJy/j80pmO9Bjxp+LQ9a2hUkfHgFhgHxZUVaeGVth8zG2kkgGdrp5VHhxMVFfvB26Ka6q6qE/UcS2lONSv+4T8niVRJz57qwctj8MNOkA3PTEfe/DP/LKMefke31YfT0xogHsLhDkx+mS8FCc01HReTjKLktk/Jh9mXwC5oKwueWWwlxI935ecn+3I2kAuOfMsgPLkoEBlwgiREC1pM7VVX1x8WmzIQVQTHd4iwnX96QewYckGRfNYWz/zwvWnjWlfcg8kRSe+68EHOGeRtC5r27fWLqRc0HNcjwpgHkI/b6czerCe8+07TWql4keJxJxhBYj3iOH7r9ZS8ck51XnOb8tGL1isimAJXodYGzakwktqHAD7MZhS+P02O+6jrg7d+yPC2ZCuS/3TOplYOCHQIhnZtR87PXTUwr83zfOwAwCyv6KP84JUQ45+DItrXLap7nOVZKQ5QxYIlbThAO6eima6Zu5XHfqGPMNWv0bLf5+vAjIa5np5DJrSwz9no/hj6CUh0iyI+SJq4RGI60lKtypMvF6MR3nHLEHOycRUQbZIyTHWl4QQLdHzuwN9lv10ouTEvNr6sFflAX2yb6w3hlCo7oBytH3rJekjb3IIOzBpeTPIejxzVlh0N9OT5MZdh4sNKYHUoWJ8mnfjdM+L4j5Q2Kgk/XiGDgEebkUxiEOQUdVpePF5uSCE+TPav/9FIRGXGiFn6NJMaU7aBsDTFBLloffFLYDpd8/bTwoSvifkj7buwLYM+h/qcnfdy5FWau1cKav+Blq/ZC0qBpo658RTC8ZtseAFDgXoQZuksM10hpP9bzD04Bx30xTGX81QbaSTNwSEEVrOtIhbDrj9OI43KH4O6zLzK+t30QxAv5zjk10RZ4+5SAdYndIlld9Y62opCfPDzRy3ubdve4ZEchpIKWTQvIxq3T5ogOhGaWBVYnkMtM2GVqvWV//46gET5SH/MdcwhACUcZ9kCpMnWH9CyyUwYvTT3UlNyV+DlS27LMPvaw7tx7qa+GfNCoCBd8S4esZpQYK/WReiS8=|pc7qpD42wxyXemdNPuwxbh8iIaryrBPu8f/DGwYdHTw=".parse().unwrap(), ).unwrap(); @@ -398,15 +400,17 @@ mod tests { } fn build_encryption_settings() -> EncryptionSettings { - EncryptionSettings::new( - &UserLoginMethod::Username { - client_id: "test".into(), - email: "test@bitwarden.com".into(), - kdf: Kdf::PBKDF2 { - iterations: 600_000.try_into().unwrap(), - }, + let master_key = MasterKey::derive( + &SensitiveVec::test(b"asdfasdfasdf"), + "test@bitwarden.com".as_bytes(), + &Kdf::PBKDF2 { + iterations: 600_000.try_into().unwrap(), }, - "asdfasdfasdf", + ) + .unwrap(); + + EncryptionSettings::new( + master_key, "2.Q/2PhzcC7GdeiMHhWguYAQ==|GpqzVdr0go0ug5cZh1n+uixeBC3oC90CIe0hd/HWA/pTRDZ8ane4fmsEIcuc8eMKUt55Y2q/fbNzsYu41YTZzzsJUSeqVjT8/iTQtgnNdpo=|dwI+uyvZ1h/iZ03VQ+/wrGEFYVewBUUl/syYgjsNMbE=".parse().unwrap(), "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(), ).unwrap() @@ -445,15 +449,15 @@ mod tests { let expected = SendView { id: "3d80dd72-2d14-4f26-812c-b0f0018aa144".parse().ok(), access_id: Some("ct2APRQtJk-BLLDwAYqhRA".to_owned()), - name: "Test".to_string(), + name: SensitiveString::test("Test"), notes: None, - key: Some("Pgui0FK85cNhBGWHAlBHBw".to_owned()), + key: Some(SensitiveString::test("Pgui0FK85cNhBGWHAlBHBw")), new_password: None, has_password: false, r#type: SendType::Text, file: None, text: Some(SendTextView { - text: Some("This is a test".to_owned()), + text: Some(SensitiveString::test("This is a test")), hidden: false, }), max_access_count: None, @@ -476,15 +480,15 @@ mod tests { let view = SendView { id: "3d80dd72-2d14-4f26-812c-b0f0018aa144".parse().ok(), access_id: Some("ct2APRQtJk-BLLDwAYqhRA".to_owned()), - name: "Test".to_string(), + name: SensitiveString::test("Test"), notes: None, - key: Some("Pgui0FK85cNhBGWHAlBHBw".to_owned()), + key: Some(SensitiveString::test("Pgui0FK85cNhBGWHAlBHBw")), new_password: None, has_password: false, r#type: SendType::Text, file: None, text: Some(SendTextView { - text: Some("This is a test".to_owned()), + text: Some(SensitiveString::test("This is a test")), hidden: false, }), max_access_count: None, @@ -514,7 +518,7 @@ mod tests { let view = SendView { id: None, access_id: Some("ct2APRQtJk-BLLDwAYqhRA".to_owned()), - name: "Test".to_string(), + name: SensitiveString::test("Test"), notes: None, key: None, new_password: None, @@ -522,7 +526,7 @@ mod tests { r#type: SendType::Text, file: None, text: Some(SendTextView { - text: Some("This is a test".to_owned()), + text: Some(SensitiveString::test("This is a test")), hidden: false, }), max_access_count: None, @@ -555,15 +559,15 @@ mod tests { let view = SendView { id: None, access_id: Some("ct2APRQtJk-BLLDwAYqhRA".to_owned()), - name: "Test".to_owned(), + name: SensitiveString::test("Test"), notes: None, - key: Some("Pgui0FK85cNhBGWHAlBHBw".to_owned()), + key: Some(SensitiveString::test("Pgui0FK85cNhBGWHAlBHBw")), 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()), + text: Some(SensitiveString::test("This is a test")), hidden: false, }), max_access_count: None, @@ -578,8 +582,8 @@ mod tests { let send: Send = view.encrypt_with_key(key).unwrap(); assert_eq!( - send.password, - Some("vTIDfdj3FTDbejmMf+mJWpYdMXsxfeSd1Sma3sjCtiQ=".to_owned()) + send.password.clone().unwrap(), + "vTIDfdj3FTDbejmMf+mJWpYdMXsxfeSd1Sma3sjCtiQ=" ); let v: SendView = send.decrypt_with_key(key).unwrap(); diff --git a/crates/bitwarden/src/vault/totp.rs b/crates/bitwarden/src/vault/totp.rs index 6ba754034..4586eda48 100644 --- a/crates/bitwarden/src/vault/totp.rs +++ b/crates/bitwarden/src/vault/totp.rs @@ -220,8 +220,8 @@ fn decode_b32(s: &str) -> Vec { 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(); + let byte_str = std::str::from_utf8(chunk).expect("The value is a valid string"); + let byte = u8::from_str_radix(byte_str, 2).expect("The value is a valid binary string"); bytes.push(byte); } diff --git a/crates/bitwarden/tests/register.rs b/crates/bitwarden/tests/register.rs index 8e523e26f..eb1b30e30 100644 --- a/crates/bitwarden/tests/register.rs +++ b/crates/bitwarden/tests/register.rs @@ -8,19 +8,19 @@ async fn test_register_initialize_crypto() { mobile::crypto::{InitUserCryptoMethod, InitUserCryptoRequest}, Client, }; - use bitwarden_crypto::Kdf; + use bitwarden_crypto::{Kdf, SensitiveString}; let mut client = Client::new(None); let email = "test@bitwarden.com"; - let password = "test123"; + let password = SensitiveString::test("test123"); let kdf = Kdf::PBKDF2 { iterations: NonZeroU32::new(600_000).unwrap(), }; let register_response = client .auth() - .make_register_keys(email.to_owned(), password.to_owned(), kdf.clone()) + .make_register_keys(email.to_owned(), password.clone(), kdf.clone()) .unwrap(); // Ensure we can initialize the crypto with the new keys @@ -32,7 +32,7 @@ async fn test_register_initialize_crypto() { private_key: register_response.keys.private.to_string(), method: InitUserCryptoMethod::Password { - password: password.to_owned(), + password, user_key: register_response.encrypted_user_key, }, }) diff --git a/crates/bw/Cargo.toml b/crates/bw/Cargo.toml index c54c35e28..cc05b67d1 100644 --- a/crates/bw/Cargo.toml +++ b/crates/bw/Cargo.toml @@ -14,15 +14,18 @@ repository.workspace = true license-file.workspace = true [dependencies] -clap = { version = "4.5.1", features = ["derive", "env"] } -color-eyre = "0.6" +bitwarden = { workspace = true, features = ["internal", "mobile"] } +bitwarden-cli = { workspace = true } +bitwarden-crypto = { workspace = true } +clap = { version = "4.5.4", features = ["derive", "env"] } +color-eyre = "0.6.3" env_logger = "0.11.1" inquire = "0.6.2" log = "0.4.20" tokio = { version = "1.36.0", features = ["rt-multi-thread", "macros"] } -bitwarden = { workspace = true, features = ["internal", "mobile"] } -bitwarden-cli = { path = "../bitwarden-cli", version = "0.1.0" } - [dev-dependencies] tempfile = "3.10.0" + +[lints] +workspace = true diff --git a/crates/bw/src/auth/login.rs b/crates/bw/src/auth/login.rs index 91e740a3a..ccc2024d9 100644 --- a/crates/bw/src/auth/login.rs +++ b/crates/bw/src/auth/login.rs @@ -7,6 +7,7 @@ use bitwarden::{ Client, }; use bitwarden_cli::text_prompt_when_none; +use bitwarden_crypto::SensitiveString; use color_eyre::eyre::{bail, Result}; use inquire::{Password, Text}; use log::{debug, error, info}; @@ -14,13 +15,15 @@ use log::{debug, error, info}; pub(crate) async fn login_password(mut client: Client, email: Option) -> Result<()> { let email = text_prompt_when_none("Email", email)?; - let password = Password::new("Password").without_confirmation().prompt()?; + let password = SensitiveString::new(Box::new( + Password::new("Password").without_confirmation().prompt()?, + )); let kdf = client.auth().prelogin(email.clone()).await?; let result = client .auth() - .login_password(&PasswordLoginRequest { + .login_password(PasswordLoginRequest { email: email.clone(), password: password.clone(), two_factor: None, @@ -48,7 +51,7 @@ pub(crate) async fn login_password(mut client: Client, email: Option) -> // Send token client .auth() - .send_two_factor_email(&TwoFactorEmailRequest { + .send_two_factor_email(TwoFactorEmailRequest { email: email.clone(), password: password.clone(), }) @@ -68,7 +71,7 @@ pub(crate) async fn login_password(mut client: Client, email: Option) -> let result = client .auth() - .login_password(&PasswordLoginRequest { + .login_password(PasswordLoginRequest { email, password, two_factor, @@ -99,11 +102,13 @@ pub(crate) async fn login_api_key( let client_id = text_prompt_when_none("Client ID", client_id)?; let client_secret = text_prompt_when_none("Client Secret", client_secret)?; - let password = Password::new("Password").without_confirmation().prompt()?; + let password = SensitiveString::new(Box::new( + Password::new("Password").without_confirmation().prompt()?, + )); let result = client .auth() - .login_api_key(&ApiKeyLoginRequest { + .login_api_key(ApiKeyLoginRequest { client_id, client_secret, password, diff --git a/crates/bw/src/main.rs b/crates/bw/src/main.rs index 6674bda1e..d973c4074 100644 --- a/crates/bw/src/main.rs +++ b/crates/bw/src/main.rs @@ -4,6 +4,7 @@ use bitwarden::{ generators::{PassphraseGeneratorRequest, PasswordGeneratorRequest}, }; use bitwarden_cli::{install_color_eyre, text_prompt_when_none, Color}; +use bitwarden_crypto::SensitiveString; use clap::{command, Args, CommandFactory, Parser, Subcommand}; use color_eyre::eyre::Result; use inquire::Password; @@ -191,11 +192,11 @@ async fn process_commands() -> Result<()> { let mut client = bitwarden::Client::new(settings); let email = text_prompt_when_none("Email", email)?; - let password = Password::new("Password").prompt()?; + let password = SensitiveString::new(Box::new(Password::new("Password").prompt()?)); client .auth() - .register(&RegisterRequest { + .register(RegisterRequest { email, name, password, diff --git a/crates/bws/CHANGELOG.md b/crates/bws/CHANGELOG.md index 642266c70..25cec1164 100644 --- a/crates/bws/CHANGELOG.md +++ b/crates/bws/CHANGELOG.md @@ -7,6 +7,8 @@ adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). ## [Unreleased] +## [0.5.0] - 2024-04-26 + ### Added - Add a `BWS_CONFIG_FILE` environment variable to specify the location of the config file (#571) diff --git a/crates/bws/Cargo.toml b/crates/bws/Cargo.toml index ccf143c47..6686b5e1e 100644 --- a/crates/bws/Cargo.toml +++ b/crates/bws/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "bws" -version = "0.4.0" +version = "0.5.0" description = """ Bitwarden Secrets Manager CLI """ @@ -18,14 +18,16 @@ license-file.workspace = true bat = { version = "0.24.0", features = [ "regex-onig", ], default-features = false } -chrono = { version = "0.4.35", features = [ +bitwarden = { workspace = true, features = ["secrets"] } +bitwarden-cli = { workspace = true } +chrono = { version = "0.4.38", features = [ "clock", "std", ], default-features = false } -clap = { version = "4.5.1", features = ["derive", "env", "string"] } -clap_complete = "4.5.0" -color-eyre = "0.6" -comfy-table = "^7.1.0" +clap = { version = "4.5.4", features = ["derive", "env", "string"] } +clap_complete = "4.5.2" +color-eyre = "0.6.3" +comfy-table = "7.1.1" directories = "5.0.1" env_logger = "0.11.1" log = "0.4.20" @@ -33,16 +35,24 @@ regex = { version = "1.10.3", features = [ "std", "perf", ], default-features = false } -serde = "^1.0.196" -serde_json = "^1.0.113" +serde = "1.0.196" +serde_json = "1.0.113" serde_yaml = "0.9" supports-color = "3.0.0" thiserror = "1.0.57" tokio = { version = "1.36.0", features = ["rt-multi-thread", "macros"] } toml = "0.8.10" -uuid = { version = "^1.7.0", features = ["serde"] } +uuid = { version = "1.7.0", features = ["serde"] } -bitwarden = { workspace = true, features = ["secrets"] } +[build-dependencies] +bitwarden-cli = { workspace = true } +clap = { version = "4.5.4", features = ["derive", "string"] } +clap_complete = "4.5.2" +clap_mangen = "0.2.20" +uuid = { version = "1.7.0" } [dev-dependencies] tempfile = "3.10.0" + +[lints] +workspace = true diff --git a/crates/bws/Dockerfile b/crates/bws/Dockerfile index 4f16f8e6c..ccf9865c9 100644 --- a/crates/bws/Dockerfile +++ b/crates/bws/Dockerfile @@ -15,27 +15,39 @@ COPY . /app # Build project WORKDIR /app/crates/bws -RUN cargo build --release +RUN cargo build --release --bin bws + +# Bundle bws dependencies +RUN mkdir /lib-bws +RUN mkdir /lib64-bws + +RUN ldd /app/target/release/bws | tr -s '[:blank:]' '\n' | grep '^/lib' | xargs -I % cp % /lib-bws +RUN ldd /app/target/release/bws | tr -s '[:blank:]' '\n' | grep '^/lib64' | xargs -I % cp % /lib64-bws + +# Make a HOME directory for the app stage +RUN mkdir -p /home/app ############################################### # App stage # ############################################### -FROM debian:bookworm-slim +FROM scratch 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 +# Set a HOME directory +COPY --from=build /home/app /home/app +ENV HOME=/home/app +WORKDIR /home/app -# Create a non-root user -RUN useradd -ms /bin/bash app +# Copy built project from the build stage +COPY --from=build /app/target/release/bws /bin/bws -# Switch to the non-root user -USER app +# Copy certs +COPY --from=build /etc/ssl/certs /etc/ssl/certs -WORKDIR /home/app +# Copy bws dependencies +COPY --from=build /lib-bws /lib +COPY --from=build /lib64-bws /lib64 ENTRYPOINT ["bws"] diff --git a/crates/bws/README.md b/crates/bws/README.md index cb9c268fb..ace5210f1 100644 --- a/crates/bws/README.md +++ b/crates/bws/README.md @@ -62,3 +62,15 @@ To use a configuration file, utilize docker ```bash docker run --rm -it -v "$HOME"/.bws:/home/app/.bws bitwarden/bws --help ``` + +## How to build manpages + +The manpages get built during compilation of the `bws` crate through the use of a build script. The +output path of this build script can be located as follows: + +``` +MANPAGES_DIR=$(cargo build -p bws --message-format json | jq -r --slurp '.[] | select (.reason == "build-script-executed") | select(.package_id|contains("crates/bws")) .out_dir') +``` + +After running the provided commands, the built manpages should be located in +`$MANPAGES_DIR/manpages` diff --git a/crates/bws/build.rs b/crates/bws/build.rs new file mode 100644 index 000000000..be0562379 --- /dev/null +++ b/crates/bws/build.rs @@ -0,0 +1,14 @@ +include!("src/cli.rs"); + +fn main() -> Result<(), std::io::Error> { + use std::{env, fs, path::Path}; + + let out_dir = env::var_os("OUT_DIR").expect("OUT_DIR exists"); + let path = Path::new(&out_dir).join("manpages"); + fs::create_dir_all(&path).expect("OUT_DIR is writable"); + + let cmd = ::command(); + clap_mangen::generate_to(cmd, &path)?; + + Ok(()) +} diff --git a/crates/bws/src/cli.rs b/crates/bws/src/cli.rs new file mode 100644 index 000000000..48d2f528e --- /dev/null +++ b/crates/bws/src/cli.rs @@ -0,0 +1,228 @@ +use std::path::PathBuf; + +use bitwarden_cli::Color; +use clap::{ArgGroup, Parser, Subcommand, ValueEnum}; +use clap_complete::Shell; +use uuid::Uuid; + +pub(crate) const ACCESS_TOKEN_KEY_VAR_NAME: &str = "BWS_ACCESS_TOKEN"; +pub(crate) const CONFIG_FILE_KEY_VAR_NAME: &str = "BWS_CONFIG_FILE"; +pub(crate) const PROFILE_KEY_VAR_NAME: &str = "BWS_PROFILE"; +pub(crate) const SERVER_URL_KEY_VAR_NAME: &str = "BWS_SERVER_URL"; + +pub(crate) const DEFAULT_CONFIG_FILENAME: &str = "config"; +pub(crate) const DEFAULT_CONFIG_DIRECTORY: &str = ".bws"; + +#[allow(non_camel_case_types)] +#[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord, ValueEnum, Debug)] +pub(crate) enum ProfileKey { + server_base, + server_api, + server_identity, + state_file_dir, +} + +#[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord, ValueEnum, Debug)] +#[allow(clippy::upper_case_acronyms)] +pub(crate) enum Output { + JSON, + YAML, + Env, + Table, + TSV, + None, +} + +#[derive(Parser, Debug)] +#[command(name = "bws", version, about = "Bitwarden Secrets CLI", long_about = None)] +pub(crate) struct Cli { + // Optional as a workaround for https://github.com/clap-rs/clap/issues/3572 + #[command(subcommand)] + pub(crate) command: Option, + + #[arg(short = 'o', long, global = true, value_enum, default_value_t = Output::JSON, help="Output format")] + pub(crate) output: Output, + + #[arg(short = 'c', long, global = true, value_enum, default_value_t = Color::Auto, help="Use colors in the output")] + pub(crate) color: Color, + + #[arg(short = 't', long, global = true, env = ACCESS_TOKEN_KEY_VAR_NAME, hide_env_values = true, help="Specify access token for the service account")] + pub(crate) access_token: Option, + + #[arg( + short = 'f', + long, + global = true, + env = CONFIG_FILE_KEY_VAR_NAME, + help = format!("[default: ~/{}/{}] Config file to use", DEFAULT_CONFIG_DIRECTORY, DEFAULT_CONFIG_FILENAME) + )] + pub(crate) config_file: Option, + + #[arg(short = 'p', long, global = true, env = PROFILE_KEY_VAR_NAME, help="Profile to use from the config file")] + pub(crate) profile: Option, + + #[arg(short = 'u', long, global = true, env = SERVER_URL_KEY_VAR_NAME, help="Override the server URL from the config file")] + pub(crate) server_url: Option, +} + +#[derive(Subcommand, Debug)] +pub(crate) enum Commands { + #[command(long_about = "Configure the CLI", arg_required_else_help(true))] + Config { + name: Option, + value: Option, + + #[arg(short = 'd', long)] + delete: bool, + }, + + #[command(long_about = "Generate shell completion files")] + Completions { shell: Option }, + + #[command(long_about = "Commands available on Projects")] + Project { + #[command(subcommand)] + cmd: ProjectCommand, + }, + #[command(long_about = "Commands available on Secrets")] + Secret { + #[command(subcommand)] + cmd: SecretCommand, + }, + #[command(long_about = "Create a single item (deprecated)", hide(true))] + Create { + #[command(subcommand)] + cmd: CreateCommand, + }, + #[command(long_about = "Delete one or more items (deprecated)", hide(true))] + Delete { + #[command(subcommand)] + cmd: DeleteCommand, + }, + #[command(long_about = "Edit a single item (deprecated)", hide(true))] + Edit { + #[command(subcommand)] + cmd: EditCommand, + }, + #[command(long_about = "Retrieve a single item (deprecated)", hide(true))] + Get { + #[command(subcommand)] + cmd: GetCommand, + }, + #[command(long_about = "List items (deprecated)", hide(true))] + List { + #[command(subcommand)] + cmd: ListCommand, + }, +} + +#[derive(Subcommand, Debug)] +pub(crate) enum SecretCommand { + Create { + key: String, + value: String, + + #[arg(help = "The ID of the project this secret will be added to")] + project_id: Uuid, + + #[arg(long, help = "An optional note to add to the secret")] + note: Option, + }, + Delete { + secret_ids: Vec, + }, + #[clap(group = ArgGroup::new("edit_field").required(true).multiple(true))] + Edit { + secret_id: Uuid, + #[arg(long, group = "edit_field")] + key: Option, + #[arg(long, group = "edit_field")] + value: Option, + #[arg(long, group = "edit_field")] + note: Option, + #[arg(long, group = "edit_field")] + project_id: Option, + }, + Get { + secret_id: Uuid, + }, + List { + project_id: Option, + }, +} + +#[derive(Subcommand, Debug)] +pub(crate) enum ProjectCommand { + Create { + name: String, + }, + Delete { + project_ids: Vec, + }, + Edit { + project_id: Uuid, + #[arg(long, group = "edit_field")] + name: String, + }, + Get { + project_id: Uuid, + }, + List, +} + +#[derive(Subcommand, Debug)] +pub(crate) enum ListCommand { + Projects, + Secrets { project_id: Option }, +} + +#[derive(Subcommand, Debug)] +pub(crate) enum GetCommand { + Project { project_id: Uuid }, + Secret { secret_id: Uuid }, +} + +#[derive(Subcommand, Debug)] +pub(crate) enum CreateCommand { + Project { + name: String, + }, + Secret { + key: String, + value: String, + + #[arg(long, help = "An optional note to add to the secret")] + note: Option, + + #[arg(long, help = "The ID of the project this secret will be added to")] + project_id: Uuid, + }, +} + +#[derive(Subcommand, Debug)] +pub(crate) enum EditCommand { + #[clap(group = ArgGroup::new("edit_field").required(true).multiple(true))] + Project { + project_id: Uuid, + #[arg(long, group = "edit_field")] + name: String, + }, + #[clap(group = ArgGroup::new("edit_field").required(true).multiple(true))] + Secret { + secret_id: Uuid, + #[arg(long, group = "edit_field")] + key: Option, + #[arg(long, group = "edit_field")] + value: Option, + #[arg(long, group = "edit_field")] + note: Option, + #[arg(long, group = "edit_field")] + project_id: Option, + }, +} + +#[derive(Subcommand, Debug)] +pub(crate) enum DeleteCommand { + Project { project_ids: Vec }, + Secret { secret_ids: Vec }, +} diff --git a/crates/bws/src/config.rs b/crates/bws/src/config.rs index 0ea1259f9..9756704f4 100644 --- a/crates/bws/src/config.rs +++ b/crates/bws/src/config.rs @@ -4,11 +4,12 @@ use std::{ path::{Path, PathBuf}, }; -use clap::ValueEnum; use color_eyre::eyre::{bail, Result}; use directories::BaseDirs; use serde::{Deserialize, Serialize}; +use crate::cli::{ProfileKey, DEFAULT_CONFIG_DIRECTORY, DEFAULT_CONFIG_FILENAME}; + #[derive(Debug, Serialize, Deserialize, Default)] pub(crate) struct Config { pub profiles: HashMap, @@ -22,16 +23,6 @@ pub(crate) struct Profile { pub state_file_dir: Option, } -// TODO: This could probably be derived with a macro if we start adding more fields -#[allow(non_camel_case_types)] -#[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord, ValueEnum, Debug)] -pub(crate) enum ProfileKey { - server_base, - server_api, - server_identity, - state_file_dir, -} - impl ProfileKey { fn update_profile_value(&self, p: &mut Profile, value: String) { match self { @@ -43,26 +34,31 @@ impl ProfileKey { } } -pub(crate) const FILENAME: &str = "config"; -pub(crate) const DIRECTORY: &str = ".bws"; - -pub(crate) fn get_config_path(config_file: Option<&Path>, ensure_folder_exists: bool) -> PathBuf { - let config_file = config_file.map(ToOwned::to_owned).unwrap_or_else(|| { - let base_dirs = BaseDirs::new().unwrap(); - base_dirs.home_dir().join(DIRECTORY).join(FILENAME) - }); +fn get_config_path(config_file: Option<&Path>, ensure_folder_exists: bool) -> Result { + let config_file = match config_file { + Some(path) => path.to_owned(), + None => { + let Some(base_dirs) = BaseDirs::new() else { + bail!("A valid home directory doesn't exist"); + }; + base_dirs + .home_dir() + .join(DEFAULT_CONFIG_DIRECTORY) + .join(DEFAULT_CONFIG_FILENAME) + } + }; if ensure_folder_exists { if let Some(parent_folder) = config_file.parent() { - std::fs::create_dir_all(parent_folder).unwrap(); + std::fs::create_dir_all(parent_folder)?; } } - config_file + Ok(config_file) } pub(crate) fn load_config(config_file: Option<&Path>, must_exist: bool) -> Result { - let file = get_config_path(config_file, false); + let file = get_config_path(config_file, false)?; let content = match file.exists() { true => read_to_string(file), @@ -75,7 +71,7 @@ pub(crate) fn load_config(config_file: Option<&Path>, must_exist: bool) -> Resul } fn write_config(config: Config, config_file: Option<&Path>) -> Result<()> { - let file = get_config_path(config_file, true); + let file = get_config_path(config_file, true)?; let content = toml::to_string_pretty(&config)?; diff --git a/crates/bws/src/main.rs b/crates/bws/src/main.rs index 6ed78f7b7..eb5c8304b 100644 --- a/crates/bws/src/main.rs +++ b/crates/bws/src/main.rs @@ -14,212 +14,19 @@ use bitwarden::{ }, }, }; -use clap::{ArgGroup, CommandFactory, Parser, Subcommand}; +use bitwarden_cli::install_color_eyre; +use clap::{CommandFactory, Parser}; use clap_complete::Shell; use color_eyre::eyre::{bail, Result}; use log::error; +use uuid::Uuid; +mod cli; mod config; mod render; mod state; -use config::ProfileKey; -use render::{serialize_response, Color, Output}; -use uuid::Uuid; - -#[derive(Parser, Debug)] -#[command(name = "bws", version, about = "Bitwarden Secrets CLI", long_about = None)] -struct Cli { - // Optional as a workaround for https://github.com/clap-rs/clap/issues/3572 - #[command(subcommand)] - command: Option, - - #[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")] - color: Color, - - #[arg(short = 't', long, global = true, env = ACCESS_TOKEN_KEY_VAR_NAME, hide_env_values = true, help="Specify access token for the service account")] - access_token: Option, - - #[arg( - short = 'f', - long, - global = true, - env = CONFIG_FILE_KEY_VAR_NAME, - help = format!("[default: ~/{}/{}] Config file to use", config::DIRECTORY, config::FILENAME) - )] - config_file: Option, - - #[arg(short = 'p', long, global = true, env = PROFILE_KEY_VAR_NAME, help="Profile to use from the config file")] - profile: Option, - - #[arg(short = 'u', long, global = true, env = SERVER_URL_KEY_VAR_NAME, help="Override the server URL from the config file")] - server_url: Option, -} - -#[derive(Subcommand, Debug)] -enum Commands { - #[command(long_about = "Configure the CLI", arg_required_else_help(true))] - Config { - name: Option, - value: Option, - - #[arg(short = 'd', long)] - delete: bool, - }, - - #[command(long_about = "Generate shell completion files")] - Completions { shell: Option }, - - #[command(long_about = "Commands available on Projects")] - Project { - #[command(subcommand)] - cmd: ProjectCommand, - }, - #[command(long_about = "Commands available on Secrets")] - Secret { - #[command(subcommand)] - cmd: SecretCommand, - }, - #[command(long_about = "Create a single item (deprecated)", hide(true))] - Create { - #[command(subcommand)] - cmd: CreateCommand, - }, - #[command(long_about = "Delete one or more items (deprecated)", hide(true))] - Delete { - #[command(subcommand)] - cmd: DeleteCommand, - }, - #[command(long_about = "Edit a single item (deprecated)", hide(true))] - Edit { - #[command(subcommand)] - cmd: EditCommand, - }, - #[command(long_about = "Retrieve a single item (deprecated)", hide(true))] - Get { - #[command(subcommand)] - cmd: GetCommand, - }, - #[command(long_about = "List items (deprecated)", hide(true))] - List { - #[command(subcommand)] - cmd: ListCommand, - }, -} - -#[derive(Subcommand, Debug)] -enum SecretCommand { - Create { - key: String, - value: String, - - #[arg(help = "The ID of the project this secret will be added to")] - project_id: Uuid, - - #[arg(long, help = "An optional note to add to the secret")] - note: Option, - }, - Delete { - secret_ids: Vec, - }, - #[clap(group = ArgGroup::new("edit_field").required(true).multiple(true))] - Edit { - secret_id: Uuid, - #[arg(long, group = "edit_field")] - key: Option, - #[arg(long, group = "edit_field")] - value: Option, - #[arg(long, group = "edit_field")] - note: Option, - #[arg(long, group = "edit_field")] - project_id: Option, - }, - Get { - secret_id: Uuid, - }, - List { - project_id: Option, - }, -} - -#[derive(Subcommand, Debug)] -enum ProjectCommand { - Create { - name: String, - }, - Delete { - project_ids: Vec, - }, - Edit { - project_id: Uuid, - #[arg(long, group = "edit_field")] - name: String, - }, - Get { - project_id: Uuid, - }, - List, -} - -#[derive(Subcommand, Debug)] -enum ListCommand { - Projects, - Secrets { project_id: Option }, -} - -#[derive(Subcommand, Debug)] -enum GetCommand { - Project { project_id: Uuid }, - Secret { secret_id: Uuid }, -} - -#[derive(Subcommand, Debug)] -enum CreateCommand { - Project { - name: String, - }, - Secret { - key: String, - value: String, - - #[arg(long, help = "An optional note to add to the secret")] - note: Option, - - #[arg(long, help = "The ID of the project this secret will be added to")] - project_id: Uuid, - }, -} - -#[derive(Subcommand, Debug)] -enum EditCommand { - #[clap(group = ArgGroup::new("edit_field").required(true).multiple(true))] - Project { - project_id: Uuid, - #[arg(long, group = "edit_field")] - name: String, - }, - #[clap(group = ArgGroup::new("edit_field").required(true).multiple(true))] - Secret { - secret_id: Uuid, - #[arg(long, group = "edit_field")] - key: Option, - #[arg(long, group = "edit_field")] - value: Option, - #[arg(long, group = "edit_field")] - note: Option, - #[arg(long, group = "edit_field")] - project_id: Option, - }, -} - -#[derive(Subcommand, Debug)] -enum DeleteCommand { - Project { project_ids: Vec }, - Secret { secret_ids: Vec }, -} +use crate::{cli::*, render::serialize_response}; #[tokio::main(flavor = "current_thread")] async fn main() -> Result<()> { @@ -228,24 +35,12 @@ async fn main() -> Result<()> { process_commands().await } -const ACCESS_TOKEN_KEY_VAR_NAME: &str = "BWS_ACCESS_TOKEN"; -const CONFIG_FILE_KEY_VAR_NAME: &str = "BWS_CONFIG_FILE"; -const PROFILE_KEY_VAR_NAME: &str = "BWS_PROFILE"; -const SERVER_URL_KEY_VAR_NAME: &str = "BWS_SERVER_URL"; - #[allow(clippy::comparison_chain)] async fn process_commands() -> Result<()> { let cli = Cli::parse(); + let color = cli.color; - let color = cli.color.is_enabled(); - if color { - color_eyre::install()?; - } else { - // Use an empty theme to disable error coloring - color_eyre::config::HookBuilder::new() - .theme(color_eyre::config::Theme::new()) - .install()?; - } + install_color_eyre(color)?; let Some(command) = cli.command else { let mut cmd = Cli::command(); @@ -328,7 +123,7 @@ async fn process_commands() -> Result<()> { let state_file_path = state::get_state_file_path( profile.and_then(|p| p.state_file_dir).map(Into::into), access_token_obj.access_token_id.to_string(), - ); + )?; let mut client = bitwarden::Client::new(settings); diff --git a/crates/bws/src/render.rs b/crates/bws/src/render.rs index f9563c81b..219e72b68 100644 --- a/crates/bws/src/render.rs +++ b/crates/bws/src/render.rs @@ -1,58 +1,34 @@ use bitwarden::secrets_manager::{projects::ProjectResponse, secrets::SecretResponse}; +use bitwarden_cli::Color; use chrono::{DateTime, Utc}; -use clap::ValueEnum; use comfy_table::Table; use serde::Serialize; -#[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord, ValueEnum, Debug)] -#[allow(clippy::upper_case_acronyms)] -pub(crate) enum Output { - JSON, - YAML, - Env, - Table, - TSV, - None, -} - -#[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord, ValueEnum, Debug)] -pub(crate) enum Color { - No, - Yes, - Auto, -} - -impl Color { - pub(crate) fn is_enabled(self) -> bool { - match self { - Color::No => false, - Color::Yes => true, - Color::Auto => supports_color::on(supports_color::Stream::Stdout).is_some(), - } - } -} +use crate::cli::Output; const ASCII_HEADER_ONLY: &str = " -- "; pub(crate) fn serialize_response, const N: usize>( data: T, output: Output, - color: bool, + color: Color, ) { match output { Output::JSON => { - let mut text = serde_json::to_string_pretty(&data).unwrap(); + let mut text = + serde_json::to_string_pretty(&data).expect("Serialize should be infallible"); // 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); } Output::YAML => { - let text = serde_yaml::to_string(&data).unwrap(); + let text = serde_yaml::to_string(&data).expect("Serialize should be infallible"); pretty_print("yaml", &text, color); } Output::Env => { - let valid_key_regex = regex::Regex::new("^[a-zA-Z_][a-zA-Z0-9_]*$").unwrap(); + let valid_key_regex = + regex::Regex::new("^[a-zA-Z_][a-zA-Z0-9_]*$").expect("regex is valid"); let mut commented_out = false; let mut text: Vec = data @@ -99,13 +75,13 @@ pub(crate) fn serialize_response, const N: usiz } } -fn pretty_print(language: &str, data: &str, color: bool) { - if color { +fn pretty_print(language: &str, data: &str, color: Color) { + if color.is_enabled() { bat::PrettyPrinter::new() .input_from_bytes(data.as_bytes()) .language(language) .print() - .unwrap(); + .expect("Input is valid"); } else { print!("{}", data); } diff --git a/crates/bws/src/state.rs b/crates/bws/src/state.rs index ef0ef07e2..9c90da81f 100644 --- a/crates/bws/src/state.rs +++ b/crates/bws/src/state.rs @@ -1,18 +1,20 @@ use std::path::PathBuf; +use color_eyre::eyre::Result; + pub(crate) fn get_state_file_path( state_file_dir: Option, access_token_id: String, -) -> Option { +) -> Result> { if let Some(mut state_file_path) = state_file_dir { state_file_path.push(access_token_id); if let Some(parent_folder) = state_file_path.parent() { - std::fs::create_dir_all(parent_folder).unwrap(); + std::fs::create_dir_all(parent_folder)?; } - return Some(state_file_path); + return Ok(Some(state_file_path)); } - None + Ok(None) } diff --git a/crates/memory-testing/Cargo.toml b/crates/memory-testing/Cargo.toml index 120fcaad1..3da4c99de 100644 --- a/crates/memory-testing/Cargo.toml +++ b/crates/memory-testing/Cargo.toml @@ -13,8 +13,8 @@ keywords.workspace = true [dependencies] bitwarden-crypto = { workspace = true } -comfy-table = "7.1.0" +comfy-table = "7.1.1" hex = "0.4.3" serde = "1.0.196" serde_json = "1.0.113" -zeroize = "1.7.0" +zeroize = { version = "1.7.0", features = ["serde"] } diff --git a/crates/memory-testing/Dockerfile b/crates/memory-testing/Dockerfile index fdcf5de00..f8556f1e2 100644 --- a/crates/memory-testing/Dockerfile +++ b/crates/memory-testing/Dockerfile @@ -3,12 +3,27 @@ ############################################### FROM rust:1.76 AS build -# Copy required project files -COPY . /app - -# Build project WORKDIR /app -RUN cargo build -p memory-testing + +# Copy dependency files and create dummy files to allow cargo to build the dependencies in a separate stage +COPY Cargo.toml Cargo.lock /app/ +COPY crates/bitwarden-crypto/Cargo.toml /app/crates/bitwarden-crypto/ +COPY crates/memory-testing/Cargo.toml /app/crates/memory-testing/ + +RUN mkdir -p /app/crates/bitwarden-crypto/src \ + && mkdir -p /app/crates/memory-testing/src \ + && touch /app/crates/bitwarden-crypto/src/lib.rs \ + && echo 'fn main(){}' > /app/crates/memory-testing/src/main.rs \ + && cargo build -p memory-testing --release + +# Delete dummy files and copy the actual source code +RUN rm /app/crates/bitwarden-crypto/src/lib.rs /app/crates/memory-testing/src/main.rs +COPY crates/bitwarden-crypto/src /app/crates/bitwarden-crypto/src +COPY crates/memory-testing/src /app/crates/memory-testing/src + +# Build the project. We use touch to force a rebuild of the now real files +RUN touch /app/crates/bitwarden-crypto/src/lib.rs /app/crates/memory-testing/src/main.rs +RUN cargo build -p memory-testing --release ############################################### # App stage # @@ -20,7 +35,8 @@ USER root RUN apt-get update && apt-get install -y --no-install-recommends gdb=13.1-3 && apt-get clean && rm -rf /var/lib/apt/lists/* -# Copy built project from the build stage -COPY --from=build /app/target/debug/memory-testing /app/target/debug/capture-dumps /app/crates/memory-testing/cases.json ./ +# Copy built project from the build stage and the cases.json file +COPY --from=build /app/target/release/memory-testing /app/target/release/capture-dumps ./ +COPY crates/memory-testing/cases.json . CMD [ "/capture-dumps", "./memory-testing", "/output" ] diff --git a/crates/memory-testing/cases.json b/crates/memory-testing/cases.json index eb85e2aca..c24cb63d6 100644 --- a/crates/memory-testing/cases.json +++ b/crates/memory-testing/cases.json @@ -1,9 +1,78 @@ { - "symmetric_key": [ + "cases": [ { - "key": "FfhVVP8fmFIZY1WmRszPmRmVCxXNWVcJffPrbkywTPtBNkgfhYGT+D9sVGizYXrPffuj2yoyWqMwF9iF5aMQhQ==", - "decrypted_key_hex": "15f85554ff1f9852196355a646cccf9919950b15cd5957097df3eb6e4cb04cfb", - "decrypted_mac_hex": "4136481f858193f83f6c5468b3617acf7dfba3db2a325aa33017d885e5a31085" + "name": "Symmetric Key", + "symmetric_key": { + "key": "FfhVVP8fmFIZY1WmRszPmRmVCxXNWVcJffPrbkywTPtBNkgfhYGT+D9sVGizYXrPffuj2yoyWqMwF9iF5aMQhQ==" + }, + "memory_lookups": [ + { + "name": "Decrypted key", + "hex": "15f85554ff1f9852196355a646cccf9919950b15cd5957097df3eb6e4cb04cfb" + }, + { + "name": "Decrypted MAC", + "hex": "4136481f858193f83f6c5468b3617acf7dfba3db2a325aa33017d885e5a31085" + } + ] + }, + + { + "name": "Master Key PBKDF2", + "master_key": { + "password": "123412341234", + "email": "test@mail.com", + + "kdf": { + "pBKDF2": { + "iterations": 100000 + } + } + }, + "memory_lookups": [ + { + "name": "Key", + "hex": "2d55ac8e33bd14ee9eee26fa651163a41049a37b3b20c914a8b6abc9a38a89d5" + }, + { + "name": "Hash B64", + "string": "gQ9O5ZhtQ23dL5r93e4BL04ATYOJVEvBAOwYsDDEJFA=" + }, + { + "name": "Hash bytes", + "hex": "810f4ee5986d436ddd2f9afdddee012f4e004d8389544bc100ec18b030c42450" + } + ] + }, + { + "name": "Master Key Argon2", + "master_key": { + "password": "asdfasdfasdf", + "email": "test@mail.com", + + "kdf": { + "argon2id": { + "iterations": 3, + "memory": 4, + "parallelism": 1 + } + } + }, + "memory_lookups": [ + { + "name": "Key", + "hex": "3bc0520a0abff0097d521ce0ee5e5b1cee301939a84742623c0c1697d7a4bd46", + "allowed_count": 3 + }, + { + "name": "Hash B64", + "string": "lHkprdORlICVJ4Umwi94Uz/nATK6Y7If7e+iFoabzh0=" + }, + { + "name": "Hash bytes", + "hex": "947929add391948095278526c22f78533fe70132ba63b21fedefa216869bce1d" + } + ] } ] } diff --git a/crates/memory-testing/run_test.sh b/crates/memory-testing/run_test.sh index 627e5dacd..8e1f84b4a 100755 --- a/crates/memory-testing/run_test.sh +++ b/crates/memory-testing/run_test.sh @@ -1,3 +1,5 @@ +set -eo pipefail + # Move to the root of the repository cd "$(dirname "$0")" cd ../../ @@ -5,16 +7,16 @@ cd ../../ BASE_DIR="./crates/memory-testing" mkdir -p $BASE_DIR/output -rm $BASE_DIR/output/* +rm $BASE_DIR/output/* || true -cargo build -p memory-testing +cargo build -p memory-testing --release if [ "$1" = "no-docker" ]; then # This specifically needs to run as root to be able to capture core dumps - sudo ./target/debug/capture-dumps ./target/debug/memory-testing $BASE_DIR + sudo ./target/release/capture-dumps ./target/release/memory-testing $BASE_DIR else docker build -f crates/memory-testing/Dockerfile -t bitwarden/memory-testing . docker run --rm -it -v $BASE_DIR:/output bitwarden/memory-testing fi -./target/debug/analyze-dumps $BASE_DIR +./target/release/analyze-dumps $BASE_DIR diff --git a/crates/memory-testing/src/bin/analyze-dumps.rs b/crates/memory-testing/src/bin/analyze-dumps.rs index fee72f2e5..d55bf8b46 100644 --- a/crates/memory-testing/src/bin/analyze-dumps.rs +++ b/crates/memory-testing/src/bin/analyze-dumps.rs @@ -1,4 +1,4 @@ -use std::{env, fmt::Display, io, path::Path, process}; +use std::{env, io, path::Path, process}; use memory_testing::*; @@ -30,26 +30,6 @@ fn comma_sep(nums: &[usize]) -> String { .join(", ") } -fn add_row( - table: &mut comfy_table::Table, - name: N, - initial_pos: &[usize], - final_pos: &[usize], - ok_cond: bool, -) -> bool { - table.add_row(vec![ - name.to_string(), - comma_sep(initial_pos), - comma_sep(final_pos), - if ok_cond { - OK.to_string() - } else { - FAIL.to_string() - }, - ]); - !ok_cond -} - fn main() -> io::Result<()> { let args: Vec = env::args().collect(); if args.len() < 2 { @@ -71,59 +51,36 @@ fn main() -> io::Result<()> { let test_string: Vec = TEST_STRING.as_bytes().to_vec(); let test_initial_pos = find_subarrays(&test_string, &initial_core); - let test_final_pos = find_subarrays(&test_string, &final_core); - - error |= add_row( - &mut table, - "Test String", - &test_initial_pos, - &test_final_pos, - !test_final_pos.is_empty(), - ); if test_initial_pos.is_empty() { println!("ERROR: Test string not found in initial core dump, is the dump valid?"); error = true; } - for (idx, case) in cases.symmetric_key.iter().enumerate() { - let key_part: Vec = hex::decode(&case.decrypted_key_hex).unwrap(); - let mac_part: Vec = hex::decode(&case.decrypted_mac_hex).unwrap(); - let key_in_b64: Vec = case.key.as_bytes().to_vec(); - - let key_initial_pos = find_subarrays(&key_part, &initial_core); - let mac_initial_pos = find_subarrays(&mac_part, &initial_core); - let b64_initial_pos = find_subarrays(&key_in_b64, &initial_core); - - let key_final_pos = find_subarrays(&key_part, &final_core); - let mac_final_pos = find_subarrays(&mac_part, &final_core); - let b64_final_pos = find_subarrays(&key_in_b64, &final_core); - - error |= add_row( - &mut table, - format!("Symm. Key, case {}", idx), - &key_initial_pos, - &key_final_pos, - key_final_pos.is_empty(), - ); - - error |= add_row( - &mut table, - format!("Symm. MAC, case {}", idx), - &mac_initial_pos, - &mac_final_pos, - mac_final_pos.is_empty(), - ); - - // TODO: At the moment we are not zeroizing the base64 key in from_str, so this test is - // ignored - add_row( - &mut table, - format!("Symm. Key in Base64, case {}", idx), - &b64_initial_pos, - &b64_final_pos, - b64_final_pos.is_empty(), - ); + for (idx, case) in cases.cases.iter().enumerate() { + for lookup in &case.memory_lookups { + let value = match &lookup.value { + MemoryLookupValue::String { string } => string.as_bytes().to_vec(), + MemoryLookupValue::Binary { hex } => hex::decode(hex).unwrap(), + }; + + let initial_pos = find_subarrays(&value, &initial_core); + let final_pos = find_subarrays(&value, &final_core); + + let name = format!("{idx}: {} / {}", case.name, lookup.name); + let ok_cond = final_pos.len() <= lookup.allowed_count.unwrap_or(0); + + table.add_row([ + name.as_str(), + &comma_sep(&initial_pos), + &comma_sep(&final_pos), + if ok_cond { OK } else { FAIL }, + ]); + + if !ok_cond { + error = true; + } + } } println!("{table}"); diff --git a/crates/memory-testing/src/bin/capture-dumps.rs b/crates/memory-testing/src/bin/capture-dumps.rs index f43905867..86a006988 100644 --- a/crates/memory-testing/src/bin/capture-dumps.rs +++ b/crates/memory-testing/src/bin/capture-dumps.rs @@ -2,9 +2,7 @@ use std::{ fs, io::{self, prelude::*}, path::Path, - process::{Command, Stdio}, - thread::sleep, - time::Duration, + process::{ChildStdin, ChildStdout, Command, Stdio}, }; fn dump_process_to_bytearray(pid: u32, output_dir: &Path, output_name: &Path) -> io::Result { @@ -19,6 +17,33 @@ fn dump_process_to_bytearray(pid: u32, output_dir: &Path, output_name: &Path) -> Ok(len) } +fn wait_dump_and_continue( + stdin: &mut ChildStdin, + stdout: &mut ChildStdout, + id: u32, + base_dir: &Path, + name: &Path, +) -> Result<(), io::Error> { + // Read the input from the process until we get the "Waiting for dump..." message + // That way we know the process is ready to be dumped, and we don't need to just sleep a fixed + // amount of time + loop { + let mut buf = [0u8; 1024]; + let read = stdout.read(&mut buf).unwrap(); + let buf_str = std::str::from_utf8(&buf[..read]).unwrap(); + if buf_str.contains("Waiting for dump...") { + break; + } + } + let dump_size = dump_process_to_bytearray(id, &base_dir.join("output"), name)?; + println!("Got memory dump of file size: {}", dump_size); + + stdin.write_all(b".")?; + stdin.flush()?; + + Ok(()) +} + fn main() -> io::Result<()> { let args: Vec = std::env::args().collect(); if args.len() < 3 { @@ -29,38 +54,19 @@ fn main() -> io::Result<()> { let binary_path = &args[1]; let base_dir: &Path = args[2].as_ref(); - println!("Memory dump capture script started"); - let mut proc = Command::new(binary_path) .arg(base_dir) - .stdout(Stdio::inherit()) + .stdout(Stdio::piped()) .stdin(Stdio::piped()) .spawn()?; let id = proc.id(); println!("Started memory testing process with PID: {}", id); - let stdin = proc.stdin.as_mut().expect("Valid stdin"); - // Wait a bit for it to process - sleep(Duration::from_secs(3)); - - // Dump the process before the variables are freed - let initial_core = - dump_process_to_bytearray(id, &base_dir.join("output"), "initial_dump.bin".as_ref())?; - println!("Initial core dump file size: {}", initial_core); - - stdin.write_all(b".")?; - stdin.flush()?; - - // Wait a bit for it to process - sleep(Duration::from_secs(1)); - - // Dump the process after the variables are freed - let final_core = - dump_process_to_bytearray(id, &base_dir.join("output"), "final_dump.bin".as_ref())?; - println!("Final core dump file size: {}", final_core); + let stdin = proc.stdin.as_mut().expect("Valid stdin"); + let stdout = proc.stdout.as_mut().expect("Valid stdin"); - stdin.write_all(b".")?; - stdin.flush()?; + wait_dump_and_continue(stdin, stdout, id, base_dir, "initial_dump.bin".as_ref())?; + wait_dump_and_continue(stdin, stdout, id, base_dir, "final_dump.bin".as_ref())?; // Wait for the process to finish and print the output let output = proc.wait()?; diff --git a/crates/memory-testing/src/lib.rs b/crates/memory-testing/src/lib.rs index e633756d1..0300e287a 100644 --- a/crates/memory-testing/src/lib.rs +++ b/crates/memory-testing/src/lib.rs @@ -1,6 +1,7 @@ use std::path::Path; -use zeroize::Zeroize; +use bitwarden_crypto::Kdf; +use zeroize::{Zeroize, Zeroizing}; pub const TEST_STRING: &str = "THIS IS USED TO CHECK THAT THE MEMORY IS DUMPED CORRECTLY"; @@ -8,22 +9,55 @@ pub fn load_cases(base_dir: &Path) -> Cases { let mut json_str = std::fs::read_to_string(base_dir.join("cases.json")).unwrap(); let cases: Cases = serde_json::from_str(&json_str).unwrap(); - // Make sure that we don't leave extra copies of the data in memory + // Make sure that we don't leave extra copies of the string data in memory json_str.zeroize(); cases } -// Note: We don't actively zeroize these structs here because we want the code in bitwarden_crypto -// to handle it for us #[derive(serde::Deserialize)] pub struct Cases { - pub symmetric_key: Vec, + pub cases: Vec, +} + +#[derive(serde::Deserialize)] +pub struct Case { + pub name: String, + #[serde(flatten)] + pub command: CaseCommand, + pub memory_lookups: Vec, +} + +// We don't actively zeroize this struct because we want the code in bitwarden_crypto +// to handle it for us +#[derive(serde::Deserialize)] +#[serde(rename_all = "snake_case")] +pub enum CaseCommand { + SymmetricKey { + key: String, + }, + MasterKey { + password: String, + email: String, + kdf: Kdf, + }, } #[derive(serde::Deserialize)] -pub struct SymmetricKeyCases { - pub key: String, +pub struct MemoryLookup { + pub name: String, + + #[serde(flatten)] + pub value: MemoryLookupValue, - pub decrypted_key_hex: String, - pub decrypted_mac_hex: String, + #[serde(default)] + pub allowed_count: Option, +} + +// We don't actually want these values to be caught by the memory testing, +// so this enum should be always zeroized +#[derive(serde::Deserialize)] +#[serde(untagged)] +pub enum MemoryLookupValue { + String { string: Zeroizing }, + Binary { hex: Zeroizing }, } diff --git a/crates/memory-testing/src/main.rs b/crates/memory-testing/src/main.rs index 96e4ae175..ce120811b 100644 --- a/crates/memory-testing/src/main.rs +++ b/crates/memory-testing/src/main.rs @@ -1,6 +1,6 @@ -use std::{env, io::Read, path::Path, process, str::FromStr}; +use std::{env, io::Read, path::Path, process}; -use bitwarden_crypto::SymmetricCryptoKey; +use bitwarden_crypto::{MasterKey, SensitiveString, SensitiveVec, SymmetricCryptoKey}; fn wait_for_dump() { println!("Waiting for dump..."); @@ -20,22 +20,40 @@ fn main() { let cases = memory_testing::load_cases(base_dir); let mut symmetric_keys = Vec::new(); - let mut symmetric_keys_as_vecs = Vec::new(); - - for case in &cases.symmetric_key { - let key = SymmetricCryptoKey::from_str(&case.key).unwrap(); - symmetric_keys_as_vecs.push(key.to_vec()); - symmetric_keys.push(key); + let mut master_keys = Vec::new(); + + for case in cases.cases { + match case.command { + memory_testing::CaseCommand::SymmetricKey { key } => { + let key = SensitiveString::new(Box::new(key)); + let key = SymmetricCryptoKey::try_from(key).unwrap(); + symmetric_keys.push((key.to_vec(), key)); + } + memory_testing::CaseCommand::MasterKey { + password, + email, + kdf, + } => { + let password: SensitiveVec = SensitiveString::new(Box::new(password)).into(); + let key = MasterKey::derive(&password, email.as_bytes(), &kdf).unwrap(); + let hash = key + .derive_master_key_hash( + &password, + bitwarden_crypto::HashPurpose::ServerAuthorization, + ) + .unwrap(); + + master_keys.push((key, hash)); + } + } } // Make a memory dump before the variables are freed wait_for_dump(); - // Use all the variables so the compiler doesn't decide to remove them - println!("{test_string} {symmetric_keys:?} {symmetric_keys_as_vecs:?}"); - - drop(symmetric_keys); - drop(symmetric_keys_as_vecs); + // Put all the variables through a black box to prevent them from being optimized out before we + // get to this point, and then drop them + let _ = std::hint::black_box((test_string, symmetric_keys, master_keys)); // After the variables are dropped, we want to make another dump wait_for_dump(); diff --git a/crates/sdk-schemas/Cargo.toml b/crates/sdk-schemas/Cargo.toml index c4cc9139b..055878b13 100644 --- a/crates/sdk-schemas/Cargo.toml +++ b/crates/sdk-schemas/Cargo.toml @@ -19,11 +19,10 @@ internal = [ ] [dependencies] -anyhow = "1.0.81" +anyhow = "1.0.82" +bitwarden = { workspace = true } +bitwarden-json = { path = "../bitwarden-json" } +bitwarden-uniffi = { path = "../bitwarden-uniffi" } itertools = "0.12.1" schemars = { version = "0.8.16", features = ["preserve_order"] } serde_json = "1.0.113" - -bitwarden = { path = "../bitwarden" } -bitwarden-json = { path = "../bitwarden-json" } -bitwarden-uniffi = { path = "../bitwarden-uniffi" } diff --git a/crates/uniffi-bindgen/Cargo.toml b/crates/uniffi-bindgen/Cargo.toml index 5ef3977bc..595252599 100644 --- a/crates/uniffi-bindgen/Cargo.toml +++ b/crates/uniffi-bindgen/Cargo.toml @@ -17,4 +17,4 @@ name = "uniffi-bindgen" path = "uniffi-bindgen.rs" [dependencies] -uniffi = { version = "=0.26.1", features = ["cli"] } +uniffi = { version = "=0.27.1", features = ["cli"] } diff --git a/languages/java/build.gradle b/languages/java/build.gradle index f044ea485..ed4a72f22 100644 --- a/languages/java/build.gradle +++ b/languages/java/build.gradle @@ -36,7 +36,7 @@ repositories { def branchName = "git branch --show-current".execute().text.trim() if (branchName == "main") { - def content = ['grep', '-o', '^version = ".*"', '../../crates/bitwarden/Cargo.toml'].execute().text.trim() + def content = ['grep', '-o', '^version = ".*"', '../../Cargo.toml'].execute().text.trim() def match = ~/version = "(.*)"/ def matcher = match.matcher(content) matcher.find() diff --git a/languages/java/gradle/wrapper/gradle-wrapper.properties b/languages/java/gradle/wrapper/gradle-wrapper.properties index ac72c34e8..b82aa23a4 100644 --- a/languages/java/gradle/wrapper/gradle-wrapper.properties +++ b/languages/java/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-8.3-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.7-bin.zip networkTimeout=10000 validateDistributionUrl=true zipStoreBase=GRADLE_USER_HOME diff --git a/languages/js/sdk-client/package-lock.json b/languages/js/sdk-client/package-lock.json index dc788c8ce..b3c3aab76 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.26", - "resolved": "https://registry.npmjs.org/@types/node/-/node-18.19.26.tgz", - "integrity": "sha512-+wiMJsIwLOYCvUqSdKTrfkS8mpTp+MPINe6+Np4TAGFWWRWiBQ5kSq9nZGCSPkzx9mvT+uEukzpX4MOSCydcvw==", + "version": "18.19.31", + "resolved": "https://registry.npmjs.org/@types/node/-/node-18.19.31.tgz", + "integrity": "sha512-ArgCD39YpyyrtFKIqMDvjz79jto5fcI/SVUs2HwB+f0dAzq68yqOdyaSivLiLugSziTpNXLQrVb7RZFmdZzbhA==", "dev": true, "dependencies": { "undici-types": "~5.26.4" @@ -147,16 +147,16 @@ } }, "node_modules/glob": { - "version": "10.3.10", - "resolved": "https://registry.npmjs.org/glob/-/glob-10.3.10.tgz", - "integrity": "sha512-fa46+tv1Ak0UPK1TOy/pZrIybNNt4HCv7SDzwyfiOZkvZLEbjsZkJBPtDHVshZjbecAoAGSC20MjLDG/qr679g==", + "version": "10.3.12", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.3.12.tgz", + "integrity": "sha512-TCNv8vJ+xz4QiqTpfOJA7HvYv+tNIRHKfUWw/q+v2jdgN4ebz+KY9tGx5J4rHP0o84mNP+ApH66HRX8us3Khqg==", "dev": true, "dependencies": { "foreground-child": "^3.1.0", - "jackspeak": "^2.3.5", + "jackspeak": "^2.3.6", "minimatch": "^9.0.1", - "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0", - "path-scurry": "^1.10.1" + "minipass": "^7.0.4", + "path-scurry": "^1.10.2" }, "bin": { "glob": "dist/esm/bin.mjs" @@ -202,18 +202,18 @@ } }, "node_modules/lru-cache": { - "version": "10.2.0", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.2.0.tgz", - "integrity": "sha512-2bIM8x+VAf6JT4bKAljS1qUWgMsqZRPGJS6FSahIMPVvctcNhyVp7AJu7quxOW9jwkryBReKZY5tY5JYv2n/7Q==", + "version": "10.2.2", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.2.2.tgz", + "integrity": "sha512-9hp3Vp2/hFQUiIwKo8XCeFVnrg8Pk3TYNPIR7tJADKi5YfcF7vEaK7avFHTlSy3kOKYaJQaalfEo6YuXdceBOQ==", "dev": true, "engines": { "node": "14 || >=16.14" } }, "node_modules/minimatch": { - "version": "9.0.3", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.3.tgz", - "integrity": "sha512-RHiac9mvaRw0x3AYRgDC1CxAP7HTcNrrECeA8YYJeWnpo+2Q5CegtZjaotWTWxDG3UeGA1coE05iH1mPjT/2mg==", + "version": "9.0.4", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.4.tgz", + "integrity": "sha512-KqWh+VchfxcMNRAJjj2tnsSJdNbHsVgnkBhTNrW7AjVo6OvLtxw8zfT9oLw1JSohlFzJ8jCoTgaoXvJ+kHt6fw==", "dev": true, "dependencies": { "brace-expansion": "^2.0.1" @@ -244,12 +244,12 @@ } }, "node_modules/path-scurry": { - "version": "1.10.1", - "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.10.1.tgz", - "integrity": "sha512-MkhCqzzBEpPvxxQ71Md0b1Kk51W01lrYvlMzSUaIzNsODdd7mqhiimSZlr+VegAz5Z6Vzt9Xg2ttE//XBhH3EQ==", + "version": "1.10.2", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.10.2.tgz", + "integrity": "sha512-7xTavNy5RQXnsjANvVvMkEjvloOinkAjv/Z6Ildz9v2RinZ4SBKTWFOVRbaF8p0vpHnyjV/UwNDdKuUv6M5qcA==", "dev": true, "dependencies": { - "lru-cache": "^9.1.1 || ^10.0.0", + "lru-cache": "^10.2.0", "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" }, "engines": { @@ -407,9 +407,9 @@ } }, "node_modules/typescript": { - "version": "5.3.3", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.3.3.tgz", - "integrity": "sha512-pXWcraxM0uxAS+tN0AG/BF2TyqmHO014Z070UsJ+pFvYuRSq8KH8DmWpnbXe0pEPDHXZV3FcAbJkijJ5oNEnWw==", + "version": "5.4.5", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.4.5.tgz", + "integrity": "sha512-vcI4UpRgg81oIRUFwR0WSIHKt11nJ7SAVlYNIu+QpqeyXP+gpQJy/Z4+F0aGxSE4MqwjyXvW/TzgkLAx2AGHwQ==", "dev": true, "bin": { "tsc": "bin/tsc", diff --git a/languages/js/wasm/package.json b/languages/js/wasm/package.json index 26379c9a6..eadbb5fb3 100644 --- a/languages/js/wasm/package.json +++ b/languages/js/wasm/package.json @@ -4,17 +4,20 @@ "files": [ "bitwarden_wasm_bg.js", "bitwarden_wasm_bg.wasm", + "bitwarden_wasm_bg.wasm.d.ts", + "bitwarden_wasm_bg.wasm.js", "bitwarden_wasm.d.ts", "bitwarden_wasm.js", "index.js", - "node/bitwarden_wasm_bg.wasm.d.ts", "node/bitwarden_wasm_bg.wasm", + "node/bitwarden_wasm_bg.wasm.d.ts", "node/bitwarden_wasm.d.ts", "node/bitwarden_wasm.js" ], "main": "node/bitwarden_wasm.js", "module": "index.js", "types": "bitwarden_wasm.d.ts", + "scripts": {}, "sideEffects": [ "./bitwarden_wasm.js", "./snippets/*" diff --git a/languages/kotlin/app/build.gradle b/languages/kotlin/app/build.gradle index ef42d43bb..188d984a2 100644 --- a/languages/kotlin/app/build.gradle +++ b/languages/kotlin/app/build.gradle @@ -6,12 +6,12 @@ plugins { android { namespace 'com.bitwarden.myapplication' - compileSdk 33 + compileSdk 34 defaultConfig { applicationId "com.bitwarden.myapplication" minSdk 28 - targetSdk 33 + targetSdk 34 versionCode 1 versionName "1.0" @@ -38,7 +38,7 @@ android { compose true } composeOptions { - kotlinCompilerExtensionVersion '1.4.8' + kotlinCompilerExtensionVersion '1.5.12' } packagingOptions { resources { diff --git a/languages/kotlin/build.gradle b/languages/kotlin/build.gradle index b682040f4..11da2e13c 100644 --- a/languages/kotlin/build.gradle +++ b/languages/kotlin/build.gradle @@ -1,7 +1,7 @@ // Top-level build file where you can add configuration options common to all sub-projects/modules. plugins { - id 'com.android.application' version '8.1.0' apply false - id 'com.android.library' version '8.1.0' apply false - id 'org.jetbrains.kotlin.android' version '1.8.22' apply false - id 'org.jetbrains.kotlin.plugin.serialization' version '1.8.22' apply false + id 'com.android.application' version '8.4.0' apply false + id 'com.android.library' version '8.4.0' apply false + id 'org.jetbrains.kotlin.android' version '1.9.23' apply false + id 'org.jetbrains.kotlin.plugin.serialization' version '1.9.23' apply false } diff --git a/languages/kotlin/doc.md b/languages/kotlin/doc.md deleted file mode 100644 index 5dce0dbb4..000000000 --- a/languages/kotlin/doc.md +++ /dev/null @@ -1,1801 +0,0 @@ -# Bitwarden Mobile SDK - -Auto generated documentation for the Bitwarden Mobile SDK. For more information please refer to the -rust crates `bitwarden` and `bitwarden-uniffi`. For code samples check the `languages/kotlin/app` -and `languages/swift/app` directories. - -## Client - -### `new` - -Initialize a new instance of the SDK client - -**Arguments**: - -- settings: Option - -**Output**: Arc - -### `crypto` - -Crypto operations - -**Arguments**: - -- self: Arc - -**Output**: Arc - -### `vault` - -Vault item operations - -**Arguments**: - -- self: Arc - -**Output**: Arc - -### `generators` - -Generator operations - -**Arguments**: - -- self: Arc - -**Output**: Arc - -### `exporters` - -Exporters - -**Arguments**: - -- self: Arc - -**Output**: Arc - -### `auth` - -Auth operations - -**Arguments**: - -- self: Arc - -**Output**: Arc - -### `echo` - -Test method, echoes back the input - -**Arguments**: - -- self: -- msg: String - -**Output**: String - -## ClientAuth - -### `password_strength` - -**API Draft:** Calculate Password Strength - -**Arguments**: - -- self: -- password: String -- email: String -- additional_inputs: Vec - -**Output**: - -### `satisfies_policy` - -Evaluate if the provided password satisfies the provided policy - -**Arguments**: - -- self: -- password: String -- strength: -- policy: [MasterPasswordPolicyOptions](#masterpasswordpolicyoptions) - -**Output**: - -### `hash_password` - -Hash the user password - -**Arguments**: - -- self: -- email: String -- password: String -- kdf_params: [Kdf](#kdf) -- purpose: [HashPurpose](#hashpurpose) - -**Output**: std::result::Result - -### `make_register_keys` - -Generate keys needed for registration process - -**Arguments**: - -- self: -- email: String -- password: String -- kdf: [Kdf](#kdf) - -**Output**: std::result::Result - -### `validate_password` - -Validate the user password - -To retrieve the user's password hash, use [`ClientAuth::hash_password`] with -`HashPurpose::LocalAuthentication` during login and persist it. If the login method has no -password, use the email OTP. - -**Arguments**: - -- self: -- password: String -- password_hash: String - -**Output**: std::result::Result<,BitwardenError> - -### `validate_password_user_key` - -Validate the user password without knowing the password hash - -Used for accounts that we know have master passwords but that have not logged in with a password. -Some example are login with device or TDE. - -This works by comparing the provided password against the encrypted user key. - -**Arguments**: - -- self: -- password: String -- encrypted_user_key: String - -**Output**: std::result::Result - -### `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 - -### `trust_device` - -Trust the current device - -**Arguments**: - -- self: - -**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` - -Encrypt cipher - -**Arguments**: - -- self: -- cipher_view: [CipherView](#cipherview) - -**Output**: std::result::Result - -### `decrypt` - -Decrypt cipher - -**Arguments**: - -- self: -- cipher: [Cipher](#cipher) - -**Output**: std::result::Result - -### `decrypt_list` - -Decrypt cipher list - -**Arguments**: - -- self: -- ciphers: Vec - -**Output**: std::result::Result - -## ClientCollections - -### `decrypt` - -Decrypt collection - -**Arguments**: - -- self: -- collection: [Collection](#collection) - -**Output**: std::result::Result - -### `decrypt_list` - -Decrypt collection list - -**Arguments**: - -- self: -- collections: Vec - -**Output**: std::result::Result - -## ClientCrypto - -### `initialize_user_crypto` - -Initialization method for the user crypto. Needs to be called before any other crypto operations. - -**Arguments**: - -- self: -- req: [InitUserCryptoRequest](#initusercryptorequest) - -**Output**: std::result::Result<,BitwardenError> - -### `initialize_org_crypto` - -Initialization method for the organization crypto. Needs to be called after -`initialize_user_crypto` but before any other crypto operations. - -**Arguments**: - -- self: -- req: [InitOrgCryptoRequest](#initorgcryptorequest) - -**Output**: std::result::Result<,BitwardenError> - -### `get_user_encryption_key` - -Get the uses's decrypted encryption key. Note: It's very important to keep this key safe, -as it can be used to decrypt all of the user's data - -**Arguments**: - -- self: - -**Output**: std::result::Result - -### `update_password` - -Update the user's password, which will re-encrypt the user's encryption key with the new -password. This returns the new encrypted user key and the new password hash. - -**Arguments**: - -- self: -- new_password: String - -**Output**: std::result::Result - -### `derive_pin_key` - -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`. - -**Arguments**: - -- self: -- pin: String - -**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` - -**API Draft:** Export user vault - -**Arguments**: - -- self: -- folders: Vec -- ciphers: Vec -- format: [ExportFormat](#exportformat) - -**Output**: std::result::Result - -### `export_organization_vault` - -**API Draft:** Export organization vault - -**Arguments**: - -- self: -- collections: Vec -- ciphers: Vec -- format: [ExportFormat](#exportformat) - -**Output**: std::result::Result - -## ClientFolders - -### `encrypt` - -Encrypt folder - -**Arguments**: - -- self: -- folder: [FolderView](#folderview) - -**Output**: std::result::Result - -### `decrypt` - -Decrypt folder - -**Arguments**: - -- self: -- folder: [Folder](#folder) - -**Output**: std::result::Result - -### `decrypt_list` - -Decrypt folder list - -**Arguments**: - -- self: -- folders: Vec - -**Output**: std::result::Result - -## ClientGenerators - -### `password` - -**API Draft:** Generate Password - -**Arguments**: - -- self: -- settings: [PasswordGeneratorRequest](#passwordgeneratorrequest) - -**Output**: std::result::Result - -### `passphrase` - -**API Draft:** Generate Passphrase - -**Arguments**: - -- self: -- settings: [PassphraseGeneratorRequest](#passphrasegeneratorrequest) - -**Output**: std::result::Result - -### `username` - -**API Draft:** Generate Username - -**Arguments**: - -- self: -- settings: UsernameGeneratorRequest - -**Output**: std::result::Result - -## ClientPasswordHistory - -### `encrypt` - -Encrypt password history - -**Arguments**: - -- self: -- password_history: [PasswordHistoryView](#passwordhistoryview) - -**Output**: std::result::Result - -### `decrypt_list` - -Decrypt password history - -**Arguments**: - -- self: -- list: Vec - -**Output**: std::result::Result - -## ClientPlatform - -### `fingerprint` - -Fingerprint (public key) - -**Arguments**: - -- self: -- req: [FingerprintRequest](#fingerprintrequest) - -**Output**: std::result::Result - -### `user_fingerprint` - -Fingerprint using logged in user's public key - -**Arguments**: - -- self: -- fingerprint_material: String - -**Output**: std::result::Result - -### `load_flags` - -Load feature flags into the client - -**Arguments**: - -- self: -- flags: std::collections::HashMap - -**Output**: std::result::Result<,BitwardenError> - -## ClientSends - -### `encrypt` - -Encrypt send - -**Arguments**: - -- self: -- send: [SendView](#sendview) - -**Output**: std::result::Result - -### `encrypt_buffer` - -Encrypt a send file in memory - -**Arguments**: - -- self: -- send: [Send](#send) -- buffer: Vec<> - -**Output**: std::result::Result - -### `encrypt_file` - -Encrypt a send file located in the file system - -**Arguments**: - -- self: -- send: [Send](#send) -- decrypted_file_path: String -- encrypted_file_path: String - -**Output**: std::result::Result<,BitwardenError> - -### `decrypt` - -Decrypt send - -**Arguments**: - -- self: -- send: [Send](#send) - -**Output**: std::result::Result - -### `decrypt_list` - -Decrypt send list - -**Arguments**: - -- self: -- sends: Vec - -**Output**: std::result::Result - -### `decrypt_buffer` - -Decrypt a send file in memory - -**Arguments**: - -- self: -- send: [Send](#send) -- buffer: Vec<> - -**Output**: std::result::Result - -### `decrypt_file` - -Decrypt a send file located in the file system - -**Arguments**: - -- self: -- send: [Send](#send) -- encrypted_file_path: String -- decrypted_file_path: String - -**Output**: std::result::Result<,BitwardenError> - -## ClientVault - -### `folders` - -Folder operations - -**Arguments**: - -- self: Arc - -**Output**: Arc - -### `collections` - -Collections operations - -**Arguments**: - -- self: Arc - -**Output**: Arc - -### `ciphers` - -Ciphers operations - -**Arguments**: - -- self: Arc - -**Output**: Arc - -### `password_history` - -Password history operations - -**Arguments**: - -- self: Arc - -**Output**: Arc - -### `sends` - -Sends operations - -**Arguments**: - -- self: Arc - -**Output**: Arc - -### `attachments` - -Attachment file operations - -**Arguments**: - -- self: Arc - -**Output**: Arc - -### `generate_totp` - -Generate a TOTP code from a provided key. - -The key can be either: - -- A base32 encoded string -- OTP Auth URI -- Steam URI - -**Arguments**: - -- self: -- key: String -- time: Option - -**Output**: std::result::Result - -# References - -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` - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
KeyTypeDescription
idstring,null
organizationIdstring,null
folderIdstring,null
collectionIdsarray
keyMore recent ciphers uses individual encryption keys to encrypt the other fields of the Cipher.
name
notes
type
login
identity
card
secureNote
favoriteboolean
reprompt
organizationUseTotpboolean
editboolean
viewPasswordboolean
localData
attachmentsarray,null
fieldsarray,null
passwordHistoryarray,null
creationDatestring
deletedDatestring,null
revisionDatestring
- -## `CipherView` - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
KeyTypeDescription
idstring,null
organizationIdstring,null
folderIdstring,null
collectionIdsarray
key
namestring
notesstring,null
type
login
identity
card
secureNote
favoriteboolean
reprompt
organizationUseTotpboolean
editboolean
viewPasswordboolean
localData
attachmentsarray,null
fieldsarray,null
passwordHistoryarray,null
creationDatestring
deletedDatestring,null
revisionDatestring
- -## `Collection` - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
KeyTypeDescription
idstring,null
organizationIdstring
name
externalIdstring,null
hidePasswordsboolean
readOnlyboolean
- -## `EncString` - - - - - - - -
KeyTypeDescription
- -## `ExportFormat` - - - - - - - - - - - - - - - -
KeyTypeDescription
EncryptedJsonobject
- - - - - - - - - - - -
KeyTypeDescription
passwordstring
-
- -## `FingerprintRequest` - - - - - - - - - - - - - - - - - -
KeyTypeDescription
fingerprintMaterialstringThe input material, used in the fingerprint generation process.
publicKeystringThe user's public key encoded with base64.
- -## `Folder` - - - - - - - - - - - - - - - - - - - - - - -
KeyTypeDescription
idstring,null
name
revisionDatestring
- -## `FolderView` - - - - - - - - - - - - - - - - - - - - - - -
KeyTypeDescription
idstring,null
namestring
revisionDatestring
- -## `HashPurpose` - - - - - - - -
KeyTypeDescription
- -## `InitOrgCryptoRequest` - - - - - - - - - - - - -
KeyTypeDescription
organizationKeysobjectThe encryption keys for all the organizations the user is a part of
- -## `InitUserCryptoMethod` - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
KeyTypeDescription
passwordobject
- - - - - - - - - - - - - - - - -
KeyTypeDescription
passwordstringThe user's master password
user_keystringThe user's encrypted symmetric crypto key
-
decryptedKeyobject
- - - - - - - - - - - -
KeyTypeDescription
decrypted_user_keystringThe user's decrypted encryption key, obtained using `get_user_encryption_key`
-
pinobject
- - - - - - - - - - - - - - - - -
KeyTypeDescription
pinstringThe user's PIN
pin_protected_user_keyThe user's symmetric crypto key, encrypted with the PIN. Use `derive_pin_key` to obtain this.
-
authRequestobject
- - - - - - - - - - - - - - - - -
KeyTypeDescription
request_private_keystringPrivate Key generated by the `crate::auth::new_auth_request`.
method
-
deviceKeyobject
- - - - - - - - - - - - - - - - - - - - - -
KeyTypeDescription
device_keystringThe device's DeviceKey
protected_device_private_keyThe Device Private Key
device_protected_user_keyThe user's symmetric crypto key, encrypted with the Device Key.
-
- -## `InitUserCryptoRequest` - - - - - - - - - - - - - - - - - - - - - - - - - - - -
KeyTypeDescription
kdfParamsThe user's KDF parameters, as received from the prelogin request
emailstringThe user's email address
privateKeystringThe user's encrypted private key
methodThe initialization method to use
- -## `Kdf` - - - - - - - - - - - - - - - - - - - - - - - -
KeyTypeDescription
pBKDF2object
- - - - - - - - - - - -
KeyTypeDescription
iterationsinteger
-
argon2idobject
- - - - - - - - - - - - - - - - - - - - - -
KeyTypeDescription
iterationsinteger
memoryinteger
parallelisminteger
-
- -## `MasterPasswordPolicyOptions` - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
KeyTypeDescription
min_complexityinteger
min_lengthinteger
require_upperboolean
require_lowerboolean
require_numbersboolean
require_specialboolean
enforce_on_loginbooleanFlag to indicate if the policy should be enforced on login. If true, and the user's password does not meet the policy requirements, the user will be forced to update their password.
- -## `PassphraseGeneratorRequest` - - - - - - - - - - - - - - - - - - - - - - - - - - - -
KeyTypeDescription
numWordsintegerNumber of words in the generated passphrase. This value must be between 3 and 20.
wordSeparatorstringCharacter separator between words in the generated passphrase. The value cannot be empty.
capitalizebooleanWhen set to true, capitalize the first letter of each word in the generated passphrase.
includeNumberbooleanWhen set to true, include a number at the end of one of the words in the generated passphrase.
- -## `PasswordGeneratorRequest` - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
KeyTypeDescription
lowercasebooleanInclude lowercase characters (a-z).
uppercasebooleanInclude uppercase characters (A-Z).
numbersbooleanInclude numbers (0-9).
specialbooleanInclude special characters: ! @ # $ % ^ & *
lengthintegerThe length of the generated password. Note that the password length must be greater than the sum of all the minimums.
avoidAmbiguousbooleanWhen set to true, the generated password will not contain ambiguous characters. The ambiguous characters are: I, O, l, 0, 1
minLowercaseinteger,nullThe minimum number of lowercase characters in the generated password. When set, the value must be between 1 and 9. This value is ignored is lowercase is false
minUppercaseinteger,nullThe 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
minNumberinteger,nullThe 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
minSpecialinteger,nullThe minimum number of special characters in the generated password. When set, the value must be between 1 and 9. This value is ignored is special is false
- -## `PasswordHistoryView` - - - - - - - - - - - - - - - - - -
KeyTypeDescription
passwordstring
lastUsedDatestring
- -## `Send` - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
KeyTypeDescription
idstring,null
accessIdstring,null
name
notes
key
passwordstring,null
type
file
text
maxAccessCountinteger,null
accessCountinteger
disabledboolean
hideEmailboolean
revisionDatestring
deletionDatestring
expirationDatestring,null
- -## `SendView` - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
KeyTypeDescription
idstring,null
accessIdstring,null
namestring
notesstring,null
keystring,nullBase64 encoded key
newPasswordstring,nullReplace 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.
hasPasswordbooleanDenote if an existing send has a password. The SDK will ignore this value when creating or updating sends.
type
file
text
maxAccessCountinteger,null
accessCountinteger
disabledboolean
hideEmailboolean
revisionDatestring
deletionDatestring
expirationDatestring,null
diff --git a/languages/kotlin/gradle/wrapper/gradle-wrapper.properties b/languages/kotlin/gradle/wrapper/gradle-wrapper.properties index 21f3b1005..9d5906979 100644 --- a/languages/kotlin/gradle/wrapper/gradle-wrapper.properties +++ b/languages/kotlin/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ #Mon Jul 24 14:16:42 CEST 2023 distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-8.0-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.7-bin.zip zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists diff --git a/languages/kotlin/sdk/build.gradle b/languages/kotlin/sdk/build.gradle index 9b3ca3585..14f6f943c 100644 --- a/languages/kotlin/sdk/build.gradle +++ b/languages/kotlin/sdk/build.gradle @@ -6,11 +6,11 @@ plugins { android { namespace 'com.bitwarden.sdk' - compileSdk 33 + compileSdk 34 defaultConfig { minSdk 28 - targetSdk 33 + targetSdk 34 testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" consumerProguardFiles "consumer-rules.pro" @@ -55,7 +55,7 @@ publishing { def branchName = "git branch --show-current".execute().text.trim() if (branchName == "main") { - def content = ['grep', '-o', '^version = ".*"', '../../crates/bitwarden/Cargo.toml'].execute().text.trim() + def content = ['grep', '-o', '^version = ".*"', '../../Cargo.toml'].execute().text.trim() def match = ~/version = "(.*)"/ def matcher = match.matcher(content) matcher.find() @@ -84,11 +84,9 @@ publishing { } dependencies { - implementation 'net.java.dev.jna:jna:5.13.0@aar' + implementation 'net.java.dev.jna:jna:5.14.0@aar' - implementation 'androidx.core:core-ktx:1.8.0' - implementation 'androidx.appcompat:appcompat:1.4.1' - implementation 'com.google.android.material:material:1.5.0' + implementation 'androidx.core:core-ktx:1.13.0' implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.7.3' testImplementation 'junit:junit:4.13.2' diff --git a/languages/ruby/README.md b/languages/ruby/README.md index e9fb61e0c..a02b1b53e 100644 --- a/languages/ruby/README.md +++ b/languages/ruby/README.md @@ -96,7 +96,18 @@ puts response ## Development +Prerequisites: + +- Ruby >= 3.0 installed +- Generate schemas `npm run schemas` + ```bash +# Navigate to the ruby language folder +cd languages/ruby + +# Make the binary folder if it doesn't exist already +mkdir -p ./bitwarden_sdk_secrets/lib/macos-arm64 + # Build and copy the bitwarden-c library cargo build --package bitwarden-c cp ../../target/debug/libbitwarden_c.dylib ./bitwarden_sdk_secrets/lib/macos-arm64/libbitwarden_c.dylib @@ -113,8 +124,8 @@ cd .. export ACCESS_TOKEN="" export ORGANIZATION_ID="" -export API_URL=https://localhost:8080/api -export IDENTITY_URL=https://localhost:8080/identity +export API_URL=http://localhost:4000 +export IDENTITY_URL=http://localhost:33656 ruby examples/example.rb ``` diff --git a/languages/ruby/bitwarden_sdk_secrets/lib/version.rb b/languages/ruby/bitwarden_sdk_secrets/lib/version.rb index 82d5fc85b..3103642a1 100644 --- a/languages/ruby/bitwarden_sdk_secrets/lib/version.rb +++ b/languages/ruby/bitwarden_sdk_secrets/lib/version.rb @@ -1,5 +1,5 @@ # frozen_string_literal: true module BitwardenSDKSecrets - VERSION = '0.1.0' + VERSION = '0.2.0' end diff --git a/languages/swift/iOS/App/Biometrics.swift b/languages/swift/iOS/App/Biometrics.swift index 2fc5ef2a3..d14223dbc 100644 --- a/languages/swift/iOS/App/Biometrics.swift +++ b/languages/swift/iOS/App/Biometrics.swift @@ -21,7 +21,6 @@ let SERVICE: String = "com.example.app" // We should separate keys for each user by appending the user_id let KEY: String = "biometric_key" - func biometricStoreValue(value: String) { var error: Unmanaged? let accessControl = SecAccessControlCreateWithFlags( @@ -29,11 +28,11 @@ func biometricStoreValue(value: String) { kSecAttrAccessibleWhenUnlockedThisDeviceOnly, .biometryCurrentSet, &error) - + guard accessControl != nil && error == nil else { fatalError("SecAccessControlCreateWithFlags failed") } - + let query = [ kSecClass: kSecClassGenericPassword, kSecAttrService: SERVICE, @@ -41,12 +40,12 @@ func biometricStoreValue(value: String) { kSecValueData: value.data(using: .utf8)!, kSecAttrAccessControl: accessControl as Any ] as CFDictionary - + // Try to delete the previous secret, if it exists // Otherwise we get `errSecDuplicateItem` SecItemDelete(query) - - let status = SecItemAdd(query, nil) + + let status = SecItemAdd(query, nil) guard status == errSecSuccess else { fatalError("Unable to store the secret: " + errToString(status: status)) } @@ -67,9 +66,9 @@ func biometricRetrieveValue() -> String? { kSecAttrAccount: KEY, kSecMatchLimit: kSecMatchLimitOne, kSecReturnData: true, - kSecReturnAttributes: true, + kSecReturnAttributes: true ] as CFDictionary - + var item: AnyObject? let status = SecItemCopyMatching(searchQuery, &item) @@ -82,11 +81,11 @@ func biometricRetrieveValue() -> String? { guard status == noErr else { fatalError("Unable to retrieve the secret: " + errToString(status: status)) } - + if let resultDictionary = item as? [String: Any], let data = resultDictionary[kSecValueData as String] as? Data { return String(decoding: data, as: UTF8.self) } - + return nil } diff --git a/languages/swift/iOS/App/ContentView.swift b/languages/swift/iOS/App/ContentView.swift index b5fc0d4f0..4a32dd9f4 100644 --- a/languages/swift/iOS/App/ContentView.swift +++ b/languages/swift/iOS/App/ContentView.swift @@ -27,29 +27,29 @@ let PIN = "1234" struct ContentView: View { private var http: URLSession - + @State private var client: Client - + @State private var accessToken: String = "" - + init() { // Disable SSL Cert validation. Don't do this in production http = URLSession( configuration: URLSessionConfiguration.default, delegate: IgnoreHttpsDelegate(), delegateQueue: nil) - + client = Client(settings: nil) } - + @State var setupBiometrics: Bool = true @State var setupPin: Bool = true @State var outputText: String = "" - + var body: some View { VStack { Toggle("Setup biometric unlock after login", isOn: $setupBiometrics).padding(.init(top: 0, leading: 20, bottom: 0, trailing: 20)) Toggle("Setup PIN unlock after login", isOn: $setupPin).padding(.init(top: 0, leading: 20, bottom: 0, trailing: 20)) - + Button(action: { Task { do { @@ -62,9 +62,9 @@ struct ContentView: View { }, label: { Text("Login with username + password") }) - + Divider().padding(30) - + Button(action: { Task { do { @@ -77,7 +77,7 @@ struct ContentView: View { }, label: { Text("Unlock with biometrics") }) - + Button(action: { Task { do { @@ -90,30 +90,42 @@ struct ContentView: View { }, label: { Text("Unlock with PIN") }) - + + Button(action: { + Task { + do { + try await authenticatorTest(clientFido: client.platform().fido2()) + } catch { + print("ERROR:", error) + } + } + }, label: { + Text("Passkey") + }) + Button(action: { client = Client(settings: nil) }, label: { Text("Lock & reset client") }).padding() - + Text("Output: " + outputText).padding(.top) } .padding() } - + func clientExamplePassword(clientAuth: ClientAuthProtocol, clientCrypto: ClientCryptoProtocol, setupBiometrics: Bool, setupPin: Bool) async throws { ////////////////////////////// Get master password hash ////////////////////////////// - + struct PreloginRequest: Codable { let email: String } struct PreloginResponse: Codable { let kdf: UInt32 let kdfIterations: UInt32 let kdfMemory: UInt32? let kdfParallelism: UInt32? - + } - + let (preloginDataJson, _) = try await http.data( for: request( method: "POST", url: IDENTITY_URL + "accounts/prelogin", @@ -123,7 +135,7 @@ struct ContentView: View { })) let preloginData = try JSONDecoder().decode( PreloginResponse.self, from: preloginDataJson) - + let kdf: Kdf if preloginData.kdf == 0 { kdf = Kdf.pbkdf2(iterations: preloginData.kdfIterations) @@ -132,19 +144,19 @@ struct ContentView: View { iterations: preloginData.kdfIterations, memory: preloginData.kdfMemory!, parallelism: preloginData.kdfParallelism!) } - + let passwordHash = try await clientAuth.hashPassword( email: EMAIL, password: PASSWORD, kdfParams: kdf, purpose: .serverAuthorization) - + ///////////////////////////// Login ///////////////////////////// - + struct LoginResponse: Codable { let Key: String let PrivateKey: String let access_token: String let refresh_token: String } - + let (loginDataJson, _) = try await http.data( for: request( method: "POST", url: IDENTITY_URL + "connect/token", @@ -154,7 +166,7 @@ struct ContentView: View { forHTTPHeaderField: "Auth-Email") r.setValue( "application/x-www-form-urlencoded", forHTTPHeaderField: "Content-Type") - + var comp = URLComponents() comp.queryItems = [ URLQueryItem(name: "scope", value: "api offline_access"), @@ -166,7 +178,7 @@ struct ContentView: View { URLQueryItem(name: "deviceName", value: "edge"), URLQueryItem(name: "grant_type", value: "password"), URLQueryItem(name: "username", value: EMAIL), - URLQueryItem(name: "password", value: passwordHash), + URLQueryItem(name: "password", value: passwordHash) ] r.httpBody = comp.percentEncodedQuery! .replacingOccurrences(of: "@", with: "%40") @@ -174,7 +186,7 @@ struct ContentView: View { .data(using: .utf8) })) let loginData = try JSONDecoder().decode(LoginResponse.self, from: loginDataJson) - + try await clientCrypto.initializeUserCrypto( req: InitUserCryptoRequest( kdfParams: kdf, @@ -185,10 +197,10 @@ struct ContentView: View { userKey: loginData.Key ) )) - + accessToken = loginData.access_token - - if (setupBiometrics) { + + if setupBiometrics { let defaults = UserDefaults.standard defaults.set(loginData.PrivateKey, forKey: "privateKey") defaults.set(preloginData.kdf, forKey: "kdfType") @@ -196,28 +208,28 @@ struct ContentView: View { defaults.set(preloginData.kdfMemory, forKey: "kdfMemory") defaults.set(preloginData.kdfParallelism, forKey: "kdfParallelism") defaults.synchronize() - + let key = try await clientCrypto.getUserEncryptionKey() biometricStoreValue(value: key) } - - if (setupPin) { + + if setupPin { let pinOptions = try await clientCrypto.derivePinKey(pin: PIN) - + let defaults = UserDefaults.standard defaults.set(loginData.PrivateKey, forKey: "privateKey") defaults.set(preloginData.kdf, forKey: "kdfType") defaults.set(preloginData.kdfIterations, forKey: "kdfIterations") defaults.set(preloginData.kdfMemory, forKey: "kdfMemory") defaults.set(preloginData.kdfParallelism, forKey: "kdfParallelism") - + defaults.set(pinOptions.encryptedPin, forKey: "encryptedPin") defaults.set(pinOptions.pinProtectedUserKey, forKey: "pinProtectedUserKey") - + defaults.synchronize() } } - + func clientExampleBiometrics(clientCrypto: ClientCryptoProtocol) async throws { let defaults = UserDefaults.standard let privateKey = defaults.string(forKey: "privateKey")! @@ -230,9 +242,9 @@ struct ContentView: View { parallelism: UInt32(defaults.integer(forKey: "kdfParallelism")) ) } - + let key = biometricRetrieveValue()! - + try await clientCrypto.initializeUserCrypto(req: InitUserCryptoRequest( kdfParams: kdf, email: EMAIL, @@ -242,7 +254,7 @@ struct ContentView: View { ) )) } - + func clientExamplePin(clientCrypto: ClientCryptoProtocol) async throws { let defaults = UserDefaults.standard let privateKey = defaults.string(forKey: "privateKey")! @@ -255,10 +267,10 @@ struct ContentView: View { parallelism: UInt32(defaults.integer(forKey: "kdfParallelism")) ) } - + let encryptedPin = defaults.string(forKey: "encryptedPin")! let pinProtectedUserKey = defaults.string(forKey: "pinProtectedUserKey")! - + try await clientCrypto.initializeUserCrypto(req: InitUserCryptoRequest( kdfParams: kdf, email: EMAIL, @@ -266,17 +278,17 @@ struct ContentView: View { method: InitUserCryptoMethod.pin(pin: PIN, pinProtectedUserKey: pinProtectedUserKey) )) } - + func decryptVault(clientCrypto: ClientCryptoProtocol, clientVault: ClientVaultProtocol) async throws { ///////////////////////////// Sync ///////////////////////////// - + struct SyncOrganization: Codable { let id: String let key: String } struct SyncProfile: Codable { let organizations: [SyncOrganization] - + } struct SyncFolder: Codable { let id: String @@ -287,7 +299,7 @@ struct ContentView: View { let profile: SyncProfile let folders: [SyncFolder] } - + let (syncDataJson, _) = try await http.data( for: request( method: "GET", url: API_URL + "sync?excludeDomains=true", @@ -295,23 +307,23 @@ struct ContentView: View { r.setValue( "Bearer " + accessToken, forHTTPHeaderField: "Authorization") })) - + let syncData = try JSONDecoder().decode(SyncResponse.self, from: syncDataJson) - + ///////////////////////////// Initialize org crypto ///////////////////////////// - + try await clientCrypto.initializeOrgCrypto( req: InitOrgCryptoRequest( organizationKeys: Dictionary.init( uniqueKeysWithValues: syncData.profile.organizations.map { ($0.id, $0.key) } ) )) - + ///////////////////////////// Decrypt some folders ///////////////////////////// - + let dateFormatter = ISO8601DateFormatter() dateFormatter.formatOptions = [.withFractionalSeconds] - + let decryptedFolders = try await clientVault.folders().decryptList( folders: syncData.folders.map { Folder( @@ -320,6 +332,40 @@ struct ContentView: View { }) print(decryptedFolders) } + + func authenticatorTest(clientFido: ClientFido2) async throws { + let ui = UserInterfaceImpl() + let cs = CredentialStoreImpl() + let authenticator = clientFido.authenticator(userInterface: ui, credentialStore: cs) + + // Make credential + try await authenticator.makeCredential(request: MakeCredentialRequest( + clientDataHash: Data(), + rp: PublicKeyCredentialRpEntity(id: "abc", name: "test"), + user: PublicKeyCredentialUserEntity(id: Data(), displayName: "b", name: "c"), + pubKeyCredParams: [PublicKeyCredentialParameters(ty: "public-key", alg: 0)], + excludeList: nil, + requireResidentKey: true, + extensions: nil + )) + + // Get Assertion + try await authenticator.getAssertion(request: GetAssertionRequest( + rpId: "", + clientDataHash: Data(), + allowList: nil, + options: Options(rk: true, uv: Uv.preferred), + extensions: nil + )) + + try await authenticator.silentlyDiscoverCredentials(rpId: "") + + // Only on android! + let client = clientFido.client(userInterface: ui, credentialStore: cs) + try await client.authenticate(origin: "test", request: "test", clientData: ClientData.defaultWithExtraData(androidPackageName: "abc")) + try await client.register(origin: "test", request: "test", clientData: ClientData.defaultWithExtraData(androidPackageName: "abc")) + } + } struct ContentView_Previews: PreviewProvider { @@ -342,9 +388,33 @@ extension IgnoreHttpsDelegate: URLSessionDelegate { _ session: URLSession, didReceive challenge: URLAuthenticationChallenge, completionHandler: @escaping (URLSession.AuthChallengeDisposition, URLCredential?) -> Void ) { - //Trust the certificate even if not valid + // Trust the certificate even if not valid let urlCredential = URLCredential(trust: challenge.protectionSpace.serverTrust!) - + completionHandler(.useCredential, urlCredential) } } + +class UserInterfaceImpl: UserInterface { + func pickCredentialForAuthentication(availableCredentials: [BitwardenSdk.Cipher]) async throws -> BitwardenSdk.CipherViewWrapper { + abort() + } + + func pickCredentialForCreation(availableCredentials: [BitwardenSdk.Cipher], newCredential: BitwardenSdk.Fido2Credential) async throws -> BitwardenSdk.CipherViewWrapper { + abort() + } + + func checkUser(options: BitwardenSdk.CheckUserOptions, credential: BitwardenSdk.CipherView?) async throws -> BitwardenSdk.CheckUserResult { + return CheckUserResult(userPresent: true, userVerified: true) + } +} + +class CredentialStoreImpl: CredentialStore { + func findCredentials(ids: [Data]?, ripId: String) async throws -> [BitwardenSdk.Cipher] { + abort() + } + + func saveCredential(cred: BitwardenSdk.Cipher) async throws { + print("SAVED CREDENTIAL") + } +} diff --git a/package-lock.json b/package-lock.json index 2c4b9b478..fac302d74 100644 --- a/package-lock.json +++ b/package-lock.json @@ -19,9 +19,9 @@ } }, "node_modules/@babel/runtime": { - "version": "7.23.9", - "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.23.9.tgz", - "integrity": "sha512-0CX6F+BI2s9dkUqr08KFrAIZgNFj75rdBU/DjCyYLIaV/quFjkk6T+EJ2LkZHyZTbEV4L5p97mNkUsHl2wLFAw==", + "version": "7.24.5", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.24.5.tgz", + "integrity": "sha512-Nms86NXrsaeU9vbBJKni6gXiEXZ4CVpYVzEjDH9Sb8vmZ3UljyA1GSOJl/6LGPO8EHLuSF9H+IxNXHPX8QHJ4g==", "dev": true, "dependencies": { "regenerator-runtime": "^0.14.0" @@ -322,9 +322,9 @@ } }, "node_modules/@tsconfig/node10": { - "version": "1.0.9", - "resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.9.tgz", - "integrity": "sha512-jNsYVVxU8v5g43Erja32laIDHXeoNvFEpX33OK4d6hljo3jDhCBDhx5dhCCTMWUojscpAagGiRkBKxpdl9fxqA==", + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.11.tgz", + "integrity": "sha512-DcRjDCujK/kCk/cUe8Xz8ZSpm8mS3mNNpta+jGCA6USEDfktlNvm1+IuZ9eTcDbNk41BHwpHHeW+N1lKCz4zOw==", "dev": true }, "node_modules/@tsconfig/node12": { @@ -346,9 +346,9 @@ "dev": true }, "node_modules/@types/node": { - "version": "20.11.19", - "resolved": "https://registry.npmjs.org/@types/node/-/node-20.11.19.tgz", - "integrity": "sha512-7xMnVEcZFu0DikYjWOlRq7NTPETrm7teqUT2WkQjrTIkEgUyyGdWsj/Zg8bEJt5TNklzbPD1X3fqfsHw3SpapQ==", + "version": "20.12.8", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.12.8.tgz", + "integrity": "sha512-NU0rJLJnshZWdE/097cdCBbyW1h4hEg0xpovcoAQYHl8dnEyp/NAOiE45pvc+Bd1Dt+2r94v2eGFpQJ4R7g+2w==", "dev": true, "peer": true, "dependencies": { @@ -923,9 +923,9 @@ } }, "node_modules/follow-redirects": { - "version": "1.15.5", - "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.5.tgz", - "integrity": "sha512-vSFWUON1B+yAw1VN4xMfxgn5fTUiaOzAJCKBwIIgT/+7CuGy9+r+5gITvP62j3RmaD5Ph65UaERdOSRGUzZtgw==", + "version": "1.15.6", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.6.tgz", + "integrity": "sha512-wWN62YITEaOpSK584EZXJafH1AGpO8RVgElfkuXbTOrPX4fIfOyEpW/CsiNd8JdYrAoOvafRTOEnvsO++qCqFA==", "dev": true, "funding": [ { @@ -1213,9 +1213,9 @@ } }, "node_modules/js-base64": { - "version": "3.7.6", - "resolved": "https://registry.npmjs.org/js-base64/-/js-base64-3.7.6.tgz", - "integrity": "sha512-NPrWuHFxFUknr1KqJRDgUQPexQF0uIJWjeT+2KjEePhitQxQEx5EJBG1lVn5/hc8aLycTpXrDOgPQ6Zq+EDiTA==", + "version": "3.7.7", + "resolved": "https://registry.npmjs.org/js-base64/-/js-base64-3.7.7.tgz", + "integrity": "sha512-7rCnleh0z2CkXhH67J8K1Ytz0b2Y+yxTPL+/KOJoa20hfnVQ/3/T6W/KflYI4bRHRagNeXeU2bkNGI3v1oS/lw==", "dev": true }, "node_modules/jsonfile": { @@ -1253,9 +1253,9 @@ } }, "node_modules/lru-cache": { - "version": "10.2.0", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.2.0.tgz", - "integrity": "sha512-2bIM8x+VAf6JT4bKAljS1qUWgMsqZRPGJS6FSahIMPVvctcNhyVp7AJu7quxOW9jwkryBReKZY5tY5JYv2n/7Q==", + "version": "10.2.2", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.2.2.tgz", + "integrity": "sha512-9hp3Vp2/hFQUiIwKo8XCeFVnrg8Pk3TYNPIR7tJADKi5YfcF7vEaK7avFHTlSy3kOKYaJQaalfEo6YuXdceBOQ==", "dev": true, "engines": { "node": "14 || >=16.14" @@ -1440,12 +1440,12 @@ } }, "node_modules/path-scurry": { - "version": "1.10.1", - "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.10.1.tgz", - "integrity": "sha512-MkhCqzzBEpPvxxQ71Md0b1Kk51W01lrYvlMzSUaIzNsODdd7mqhiimSZlr+VegAz5Z6Vzt9Xg2ttE//XBhH3EQ==", + "version": "1.10.2", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.10.2.tgz", + "integrity": "sha512-7xTavNy5RQXnsjANvVvMkEjvloOinkAjv/Z6Ildz9v2RinZ4SBKTWFOVRbaF8p0vpHnyjV/UwNDdKuUv6M5qcA==", "dev": true, "dependencies": { - "lru-cache": "^9.1.1 || ^10.0.0", + "lru-cache": "^10.2.0", "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" }, "engines": { @@ -1639,16 +1639,16 @@ } }, "node_modules/rimraf/node_modules/glob": { - "version": "10.3.10", - "resolved": "https://registry.npmjs.org/glob/-/glob-10.3.10.tgz", - "integrity": "sha512-fa46+tv1Ak0UPK1TOy/pZrIybNNt4HCv7SDzwyfiOZkvZLEbjsZkJBPtDHVshZjbecAoAGSC20MjLDG/qr679g==", + "version": "10.3.12", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.3.12.tgz", + "integrity": "sha512-TCNv8vJ+xz4QiqTpfOJA7HvYv+tNIRHKfUWw/q+v2jdgN4ebz+KY9tGx5J4rHP0o84mNP+ApH66HRX8us3Khqg==", "dev": true, "dependencies": { "foreground-child": "^3.1.0", - "jackspeak": "^2.3.5", + "jackspeak": "^2.3.6", "minimatch": "^9.0.1", - "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0", - "path-scurry": "^1.10.1" + "minipass": "^7.0.4", + "path-scurry": "^1.10.2" }, "bin": { "glob": "dist/esm/bin.mjs" @@ -1661,9 +1661,9 @@ } }, "node_modules/rimraf/node_modules/minimatch": { - "version": "9.0.3", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.3.tgz", - "integrity": "sha512-RHiac9mvaRw0x3AYRgDC1CxAP7HTcNrrECeA8YYJeWnpo+2Q5CegtZjaotWTWxDG3UeGA1coE05iH1mPjT/2mg==", + "version": "9.0.4", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.4.tgz", + "integrity": "sha512-KqWh+VchfxcMNRAJjj2tnsSJdNbHsVgnkBhTNrW7AjVo6OvLtxw8zfT9oLw1JSohlFzJ8jCoTgaoXvJ+kHt6fw==", "dev": true, "dependencies": { "brace-expansion": "^2.0.1" @@ -2128,10 +2128,13 @@ } }, "node_modules/yaml": { - "version": "2.3.4", - "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.3.4.tgz", - "integrity": "sha512-8aAvwVUSHpfEqTQ4w/KMlf3HcRdt50E5ODIQJBw1fQ5RL34xabzxtUlzTXVqc4rkZsPbvrXKWnABCD7kWSmocA==", + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.4.2.tgz", + "integrity": "sha512-B3VqDZ+JAg1nZpaEmWtTXUlBneoGx6CPM9b0TENK6aoSu5t73dItudwdgmi6tHlIZZId4dZ9skcAQ2UbcyAeVA==", "dev": true, + "bin": { + "yaml": "bin.mjs" + }, "engines": { "node": ">= 14" } diff --git a/support/openapi-template/Cargo.mustache b/support/openapi-template/Cargo.mustache index 6417d15cd..08f1a942e 100644 --- a/support/openapi-template/Cargo.mustache +++ b/support/openapi-template/Cargo.mustache @@ -65,7 +65,7 @@ reqwest-middleware = "0.2.0" {{/supportMiddleware}} [dependencies.reqwest] version = "^0.11" -features = ["json", "multipart"] +features = ["http2", "json", "multipart"] default-features = false {{/supportAsync}} {{/reqwest}}