diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 9665d40b07e..7a7bb31ea54 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -19,9 +19,6 @@ apps/web/src/connectors @bitwarden/team-auth-dev bitwarden_license/bit-web/src/app/auth @bitwarden/team-auth-dev libs/angular/src/auth @bitwarden/team-auth-dev libs/common/src/auth @bitwarden/team-auth-dev -# biometrics -apps/desktop/src/services/native-messaging.service.ts @bitwarden/team-auth-dev -app/browser/src/background/nativeMessaging.background.ts @bitwarden/team-auth-dev ## Tools team files ## apps/browser/src/tools @bitwarden/team-tools-dev @@ -111,7 +108,11 @@ apps/desktop/src/key-management @bitwarden/team-key-management-dev apps/web/src/key-management @bitwarden/team-key-management-dev apps/browser/src/key-management @bitwarden/team-key-management-dev apps/cli/src/key-management @bitwarden/team-key-management-dev -libs/common/src/key-management @bitwarden/team-key-management-dev +libs/key-management @bitwarden/team-key-management-dev + +apps/desktop/destkop_native/core/src/biometric/ @bitwarden/team-key-management-dev +apps/desktop/src/services/native-messaging.service.ts @bitwarden/team-key-management-dev +apps/browser/src/background/nativeMessaging.background.ts @bitwarden/team-key-management-dev ## DevOps team files ## /.github/workflows @bitwarden/dept-devops diff --git a/.github/whitelist-capital-letters.txt b/.github/whitelist-capital-letters.txt index b09829f7f4c..c38e0241f79 100644 --- a/.github/whitelist-capital-letters.txt +++ b/.github/whitelist-capital-letters.txt @@ -20,6 +20,7 @@ ./libs/billing/README.md ./libs/common/src/tools/integration/README.md ./libs/platform/README.md +./libs/key-management/README.md ./libs/tools/README.md ./libs/tools/export/vault-export/README.md ./libs/tools/send/README.md diff --git a/.github/workflows/auto-branch-updater.yml b/.github/workflows/auto-branch-updater.yml index e2f181680d9..372aa92eba0 100644 --- a/.github/workflows/auto-branch-updater.yml +++ b/.github/workflows/auto-branch-updater.yml @@ -29,7 +29,7 @@ jobs: run: echo "branch=${GITHUB_REF#refs/heads/}" >> $GITHUB_OUTPUT - name: Checkout repo - uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7 + uses: actions/checkout@d632683dd7b4114ad314bca15554477dd762a938 # v4.2.0 with: ref: 'eu-web-${{ steps.setup.outputs.branch }}' fetch-depth: 0 diff --git a/.github/workflows/build-browser.yml b/.github/workflows/build-browser.yml index 562567ffe77..4b25c588090 100644 --- a/.github/workflows/build-browser.yml +++ b/.github/workflows/build-browser.yml @@ -43,7 +43,7 @@ jobs: node_version: ${{ steps.retrieve-node-version.outputs.node_version }} steps: - name: Checkout repo - uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7 + uses: actions/checkout@d632683dd7b4114ad314bca15554477dd762a938 # v4.2.0 - name: Get Package Version id: gen_vars @@ -73,7 +73,7 @@ jobs: working-directory: apps/browser steps: - name: Checkout repo - uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7 + uses: actions/checkout@d632683dd7b4114ad314bca15554477dd762a938 # v4.2.0 - name: Testing locales - extName length run: | @@ -111,7 +111,7 @@ jobs: _NODE_VERSION: ${{ needs.setup.outputs.node_version }} steps: - name: Checkout repo - uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7 + uses: actions/checkout@d632683dd7b4114ad314bca15554477dd762a938 # v4.2.0 - name: Set up Node uses: actions/setup-node@0a44ba7841725637a19e28fa30b79a866c81b0a6 # v4.0.4 @@ -254,7 +254,7 @@ jobs: _NODE_VERSION: ${{ needs.setup.outputs.node_version }} steps: - name: Checkout repo - uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7 + uses: actions/checkout@d632683dd7b4114ad314bca15554477dd762a938 # v4.2.0 - name: Set up Node uses: actions/setup-node@0a44ba7841725637a19e28fa30b79a866c81b0a6 # v4.0.4 @@ -367,7 +367,7 @@ jobs: - build-safari steps: - name: Checkout repo - uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7 + uses: actions/checkout@d632683dd7b4114ad314bca15554477dd762a938 # v4.2.0 - name: Login to Azure uses: Azure/login@e15b166166a8746d1a47596803bd8c1b595455cf # v1.6.0 diff --git a/.github/workflows/build-cli.yml b/.github/workflows/build-cli.yml index bccfc6d57ce..b5b212423ab 100644 --- a/.github/workflows/build-cli.yml +++ b/.github/workflows/build-cli.yml @@ -43,7 +43,7 @@ jobs: node_version: ${{ steps.retrieve-node-version.outputs.node_version }} steps: - name: Checkout repo - uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7 + uses: actions/checkout@d632683dd7b4114ad314bca15554477dd762a938 # v4.2.0 - name: Get Package Version id: retrieve-package-version @@ -84,7 +84,7 @@ jobs: _WIN_PKG_VERSION: 3.5 steps: - name: Checkout repo - uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7 + uses: actions/checkout@d632683dd7b4114ad314bca15554477dd762a938 # v4.2.0 - name: Setup Unix Vars run: | @@ -162,7 +162,7 @@ jobs: _WIN_PKG_VERSION: 3.5 steps: - name: Checkout repo - uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7 + uses: actions/checkout@d632683dd7b4114ad314bca15554477dd762a938 # v4.2.0 - name: Setup Windows builder run: | @@ -312,7 +312,7 @@ jobs: _PACKAGE_VERSION: ${{ needs.setup.outputs.package_version }} steps: - name: Checkout repo - uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7 + uses: actions/checkout@d632683dd7b4114ad314bca15554477dd762a938 # v4.2.0 - name: Print environment run: | diff --git a/.github/workflows/build-desktop.yml b/.github/workflows/build-desktop.yml index 05039c3982b..5022184bd05 100644 --- a/.github/workflows/build-desktop.yml +++ b/.github/workflows/build-desktop.yml @@ -38,7 +38,7 @@ jobs: runs-on: ubuntu-22.04 steps: - name: Checkout repo - uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7 + uses: actions/checkout@d632683dd7b4114ad314bca15554477dd762a938 # v4.2.0 - name: Verify run: | @@ -67,7 +67,7 @@ jobs: working-directory: apps/desktop steps: - name: Checkout repo - uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7 + uses: actions/checkout@d632683dd7b4114ad314bca15554477dd762a938 # v4.2.0 - name: Get Package Version id: retrieve-version @@ -140,7 +140,7 @@ jobs: working-directory: apps/desktop steps: - name: Checkout repo - uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7 + uses: actions/checkout@d632683dd7b4114ad314bca15554477dd762a938 # v4.2.0 - name: Set up Node uses: actions/setup-node@0a44ba7841725637a19e28fa30b79a866c81b0a6 # v4.0.4 @@ -174,20 +174,21 @@ jobs: with: path: | apps/desktop/desktop_native/napi/*.node + apps/desktop/desktop_native/dist/* ${{ env.RUNNER_TEMP }}/.cargo/registry ${{ env.RUNNER_TEMP }}/.cargo/git key: rust-${{ runner.os }}-${{ hashFiles('apps/desktop/desktop_native/**/*') }} - name: Build Native Module if: steps.cache.outputs.cache-hit != 'true' - working-directory: apps/desktop/desktop_native/napi + working-directory: apps/desktop/desktop_native env: PKG_CONFIG_ALLOW_CROSS: true PKG_CONFIG_ALL_STATIC: true TARGET: musl run: | rustup target add x86_64-unknown-linux-musl - npm run build:cross-platform + node build.js cross-platform - name: Build application run: npm run dist:lin @@ -249,7 +250,7 @@ jobs: NODE_OPTIONS: --max_old_space_size=4096 steps: - name: Checkout repo - uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7 + uses: actions/checkout@d632683dd7b4114ad314bca15554477dd762a938 # v4.2.0 - name: Set up Node uses: actions/setup-node@0a44ba7841725637a19e28fa30b79a866c81b0a6 # v4.0.4 @@ -301,13 +302,15 @@ jobs: uses: actions/cache@0c45773b623bea8c8e75f6c82b208c3cf94ea4f9 # v4.0.2 id: cache with: - path: apps/desktop/desktop_native/napi/*.node + path: | + apps/desktop/desktop_native/napi/*.node + apps/desktop/desktop_native/dist/* key: rust-${{ runner.os }}-${{ hashFiles('apps/desktop/desktop_native/**/*') }} - name: Build Native Module if: steps.cache.outputs.cache-hit != 'true' - working-directory: apps/desktop/desktop_native/napi - run: npm run build:cross-platform + working-directory: apps/desktop/desktop_native + run: node build.js cross-platform - name: Build & Sign (dev) env: @@ -455,7 +458,7 @@ jobs: working-directory: apps/desktop steps: - name: Checkout repo - uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7 + uses: actions/checkout@d632683dd7b4114ad314bca15554477dd762a938 # v4.2.0 - name: Set up Node uses: actions/setup-node@0a44ba7841725637a19e28fa30b79a866c81b0a6 # v4.0.4 @@ -584,13 +587,15 @@ jobs: uses: actions/cache@0c45773b623bea8c8e75f6c82b208c3cf94ea4f9 # v4.0.2 id: cache with: - path: apps/desktop/desktop_native/napi/*.node + path: | + apps/desktop/desktop_native/napi/*.node + apps/desktop/desktop_native/dist/* key: rust-${{ runner.os }}-${{ hashFiles('apps/desktop/desktop_native/**/*') }} - name: Build Native Module if: steps.cache.outputs.cache-hit != 'true' - working-directory: apps/desktop/desktop_native/napi - run: npm run build:cross-platform + working-directory: apps/desktop/desktop_native + run: node build.js cross-platform - name: Build application (dev) run: npm run build @@ -619,7 +624,7 @@ jobs: working-directory: apps/desktop steps: - name: Checkout repo - uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7 + uses: actions/checkout@d632683dd7b4114ad314bca15554477dd762a938 # v4.2.0 - name: Set up Node uses: actions/setup-node@0a44ba7841725637a19e28fa30b79a866c81b0a6 # v4.0.4 @@ -748,13 +753,15 @@ jobs: uses: actions/cache@0c45773b623bea8c8e75f6c82b208c3cf94ea4f9 # v4.0.2 id: cache with: - path: apps/desktop/desktop_native/napi/*.node + path: | + apps/desktop/desktop_native/napi/*.node + apps/desktop/desktop_native/dist/* key: rust-${{ runner.os }}-${{ hashFiles('apps/desktop/desktop_native/**/*') }} - name: Build Native Module if: steps.cache.outputs.cache-hit != 'true' - working-directory: apps/desktop/desktop_native/napi - run: npm run build:cross-platform + working-directory: apps/desktop/desktop_native + run: node build.js cross-platform - name: Build if: steps.build-cache.outputs.cache-hit != 'true' @@ -836,7 +843,7 @@ jobs: working-directory: apps/desktop steps: - name: Checkout repo - uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7 + uses: actions/checkout@d632683dd7b4114ad314bca15554477dd762a938 # v4.2.0 - name: Set up Node uses: actions/setup-node@0a44ba7841725637a19e28fa30b79a866c81b0a6 # v4.0.4 @@ -972,13 +979,15 @@ jobs: uses: actions/cache@0c45773b623bea8c8e75f6c82b208c3cf94ea4f9 # v4.0.2 id: cache with: - path: apps/desktop/desktop_native/napi/*.node + path: | + apps/desktop/desktop_native/napi/*.node + apps/desktop/desktop_native/dist/* key: rust-${{ runner.os }}-${{ hashFiles('apps/desktop/desktop_native/**/*') }} - name: Build Native Module if: steps.cache.outputs.cache-hit != 'true' - working-directory: apps/desktop/desktop_native/napi - run: npm run build:cross-platform + working-directory: apps/desktop/desktop_native + run: node build.js cross-platform - name: Build if: steps.build-cache.outputs.cache-hit != 'true' @@ -1081,7 +1090,7 @@ jobs: working-directory: apps/desktop steps: - name: Checkout repo - uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7 + uses: actions/checkout@d632683dd7b4114ad314bca15554477dd762a938 # v4.2.0 - name: Set up Node uses: actions/setup-node@0a44ba7841725637a19e28fa30b79a866c81b0a6 # v4.0.4 @@ -1205,13 +1214,15 @@ jobs: uses: actions/cache@0c45773b623bea8c8e75f6c82b208c3cf94ea4f9 # v4.0.2 id: cache with: - path: apps/desktop/desktop_native/napi/*.node + path: | + apps/desktop/desktop_native/napi/*.node + apps/desktop/desktop_native/dist/* key: rust-${{ runner.os }}-${{ hashFiles('apps/desktop/desktop_native/**/*') }} - name: Build Native Module if: steps.cache.outputs.cache-hit != 'true' - working-directory: apps/desktop/desktop_native/napi - run: npm run build:cross-platform + working-directory: apps/desktop/desktop_native + run: node build.js cross-platform - name: Build if: steps.build-cache.outputs.cache-hit != 'true' @@ -1270,7 +1281,7 @@ jobs: runs-on: ubuntu-22.04 steps: - name: Checkout repo - uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7 + uses: actions/checkout@d632683dd7b4114ad314bca15554477dd762a938 # v4.2.0 - name: Login to Azure uses: Azure/login@e15b166166a8746d1a47596803bd8c1b595455cf # v1.6.0 diff --git a/.github/workflows/build-web.yml b/.github/workflows/build-web.yml index 54e993edb33..4551a7baac8 100644 --- a/.github/workflows/build-web.yml +++ b/.github/workflows/build-web.yml @@ -45,7 +45,7 @@ jobs: node_version: ${{ steps.retrieve-node-version.outputs.node_version }} steps: - name: Checkout repo - uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7 + uses: actions/checkout@d632683dd7b4114ad314bca15554477dd762a938 # v4.2.0 - name: Get GitHub sha as version id: version @@ -91,7 +91,7 @@ jobs: steps: - name: Checkout repo - uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7 + uses: actions/checkout@d632683dd7b4114ad314bca15554477dd762a938 # v4.2.0 - name: Set up Node uses: actions/setup-node@0a44ba7841725637a19e28fa30b79a866c81b0a6 # v4.0.4 @@ -157,7 +157,7 @@ jobs: _VERSION: ${{ needs.setup.outputs.version }} steps: - name: Checkout repo - uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7 + uses: actions/checkout@d632683dd7b4114ad314bca15554477dd762a938 # v4.2.0 - name: Check Branch to Publish env: @@ -234,7 +234,7 @@ jobs: run: echo "name=$_AZ_REGISTRY/${PROJECT_NAME}:${IMAGE_TAG}" >> $GITHUB_OUTPUT - name: Build Docker image - uses: docker/build-push-action@5cd11c3a4ced054e52742c5fd54dca954e0edd85 # v6.7.0 + uses: docker/build-push-action@32945a339266b759abcbdc89316275140b0fc960 # v6.8.0 with: context: apps/web file: apps/web/Dockerfile @@ -255,7 +255,7 @@ jobs: runs-on: ubuntu-22.04 steps: - name: Checkout repo - uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7 + uses: actions/checkout@d632683dd7b4114ad314bca15554477dd762a938 # v4.2.0 - name: Login to Azure uses: Azure/login@e15b166166a8746d1a47596803bd8c1b595455cf # v1.6.0 diff --git a/.github/workflows/chromatic.yml b/.github/workflows/chromatic.yml index 272b68139ac..b52ecc1c40a 100644 --- a/.github/workflows/chromatic.yml +++ b/.github/workflows/chromatic.yml @@ -25,7 +25,7 @@ jobs: steps: - name: Check out repo - uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7 + uses: actions/checkout@d632683dd7b4114ad314bca15554477dd762a938 # v4.2.0 with: ref: ${{ github.event.pull_request.head.sha }} fetch-depth: 0 @@ -57,7 +57,7 @@ jobs: run: npm run build-storybook:ci - name: Publish to Chromatic - uses: chromaui/action@c883154c39671e194613be2f09ff4d17bb95f1e6 # v11.10.3 + uses: chromaui/action@f4e60a7072abcac4203f4ca50613f28e199a52ba # v11.10.4 with: token: ${{ secrets.GITHUB_TOKEN }} projectToken: ${{ secrets.CHROMATIC_PROJECT_TOKEN }} diff --git a/.github/workflows/crowdin-pull.yml b/.github/workflows/crowdin-pull.yml index 527dedb5a86..b319e5365f7 100644 --- a/.github/workflows/crowdin-pull.yml +++ b/.github/workflows/crowdin-pull.yml @@ -23,7 +23,7 @@ jobs: crowdin_project_id: "308189" steps: - name: Checkout repo - uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7 + uses: actions/checkout@d632683dd7b4114ad314bca15554477dd762a938 # v4.2.0 - name: Login to Azure uses: Azure/login@e15b166166a8746d1a47596803bd8c1b595455cf # v1.6.0 diff --git a/.github/workflows/deploy-web.yml b/.github/workflows/deploy-web.yml index 7551538b3a1..d0f791aa000 100644 --- a/.github/workflows/deploy-web.yml +++ b/.github/workflows/deploy-web.yml @@ -71,6 +71,7 @@ jobs: retrieve-secrets-keyvault: ${{ steps.config.outputs.retrieve-secrets-keyvault }} sync-utility: ${{ steps.config.outputs.sync-utility }} sync-delete-destination-files: ${{ steps.config.outputs.sync-delete-destination-files }} + slack-channel-name: ${{ steps.config.outputs.slack-channel-name }} steps: - name: Configure id: config @@ -86,6 +87,7 @@ jobs: echo "environment-artifact=web-*-cloud-QA.zip" >> $GITHUB_OUTPUT echo "environment-name=Web Vault - US QA Cloud" >> $GITHUB_OUTPUT echo "environment-url=http://vault.$ENV_NAME_LOWER.bitwarden.pw" >> $GITHUB_OUTPUT + echo "slack-channel-name=alerts-deploy-qa" >> $GITHUB_OUTPUT ;; "EUQA") echo "azure-login-creds=AZURE_KV_EU_QA_SERVICE_PRINCIPAL" >> $GITHUB_OUTPUT @@ -93,6 +95,7 @@ jobs: echo "environment-artifact=web-*-cloud-euqa.zip" >> $GITHUB_OUTPUT echo "environment-name=Web Vault - EU QA Cloud" >> $GITHUB_OUTPUT echo "environment-url=http://vault.$ENV_NAME_LOWER.bitwarden.pw" >> $GITHUB_OUTPUT + echo "slack-channel-name=alerts-deploy-qa" >> $GITHUB_OUTPUT ;; "USPROD") echo "azure-login-creds=AZURE_KV_US_PROD_SERVICE_PRINCIPAL" >> $GITHUB_OUTPUT @@ -100,6 +103,7 @@ jobs: echo "environment-artifact=web-*-cloud-COMMERCIAL.zip" >> $GITHUB_OUTPUT echo "environment-name=Web Vault - US Production Cloud" >> $GITHUB_OUTPUT echo "environment-url=http://vault.bitwarden.com" >> $GITHUB_OUTPUT + echo "slack-channel-name=alerts-deploy-prd" >> $GITHUB_OUTPUT ;; "EUPROD") echo "azure-login-creds=AZURE_KV_EU_PRD_SERVICE_PRINCIPAL" >> $GITHUB_OUTPUT @@ -107,6 +111,7 @@ jobs: echo "environment-artifact=web-*-cloud-euprd.zip" >> $GITHUB_OUTPUT echo "environment-name=Web Vault - EU Production Cloud" >> $GITHUB_OUTPUT echo "environment-url=http://vault.bitwarden.eu" >> $GITHUB_OUTPUT + echo "slack-channel-name=alerts-deploy-prd" >> $GITHUB_OUTPUT ;; "USDEV") echo "azure-login-creds=AZURE_KV_US_DEV_SERVICE_PRINCIPAL" >> $GITHUB_OUTPUT @@ -114,6 +119,7 @@ jobs: echo "environment-artifact=web-*-cloud-usdev.zip" >> $GITHUB_OUTPUT echo "environment-name=Web Vault - US Development Cloud" >> $GITHUB_OUTPUT echo "environment-url=http://vault.$ENV_NAME_LOWER.bitwarden.pw" >> $GITHUB_OUTPUT + echo "slack-channel-name=alerts-deploy-dev" >> $GITHUB_OUTPUT ;; esac # Set the sync utility to use for deployment to the environment (az-sync or azcopy) @@ -267,7 +273,7 @@ jobs: project: Clients environment: ${{ needs.setup.outputs.environment-name }} tag: ${{ inputs.branch-or-tag }} - slack-channel: alerts-deploy-qa + slack-channel: ${{ needs.setup.outputs.slack-channel-name }} event: 'start' commit-sha: ${{ needs.artifact-check.outputs.artifact-build-commit }} url: https://github.com/bitwarden/clients/actions/runs/${{ github.run_id }} diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index 3f72d62214b..ad8c5dd40dc 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -21,7 +21,7 @@ jobs: runs-on: ubuntu-22.04 steps: - name: Checkout repo - uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7 + uses: actions/checkout@d632683dd7b4114ad314bca15554477dd762a938 # v4.2.0 - name: Lint filenames (no capital characters) run: | diff --git a/.github/workflows/publish-cli.yml b/.github/workflows/publish-cli.yml index 6581e260900..f9c4b85e8ab 100644 --- a/.github/workflows/publish-cli.yml +++ b/.github/workflows/publish-cli.yml @@ -92,7 +92,7 @@ jobs: _PKG_VERSION: ${{ needs.setup.outputs.release-version }} steps: - name: Checkout repo - uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7 + uses: actions/checkout@d632683dd7b4114ad314bca15554477dd762a938 # v4.2.0 - name: Login to Azure uses: Azure/login@e15b166166a8746d1a47596803bd8c1b595455cf # v1.6.0 @@ -129,7 +129,7 @@ jobs: _PKG_VERSION: ${{ needs.setup.outputs.release-version }} steps: - name: Checkout repo - uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7 + uses: actions/checkout@d632683dd7b4114ad314bca15554477dd762a938 # v4.2.0 - name: Login to Azure uses: Azure/login@e15b166166a8746d1a47596803bd8c1b595455cf # v1.6.0 @@ -169,7 +169,7 @@ jobs: _PKG_VERSION: ${{ needs.setup.outputs.release-version }} steps: - name: Checkout repo - uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7 + uses: actions/checkout@d632683dd7b4114ad314bca15554477dd762a938 # v4.2.0 - name: Login to Azure uses: Azure/login@e15b166166a8746d1a47596803bd8c1b595455cf # v1.6.0 diff --git a/.github/workflows/publish-desktop.yml b/.github/workflows/publish-desktop.yml index d12072c7e6d..0816a86b1b5 100644 --- a/.github/workflows/publish-desktop.yml +++ b/.github/workflows/publish-desktop.yml @@ -184,7 +184,7 @@ jobs: _RELEASE_TAG: ${{ needs.setup.outputs.tag-name }} steps: - name: Checkout Repo - uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7 + uses: actions/checkout@d632683dd7b4114ad314bca15554477dd762a938 # v4.2.0 - name: Login to Azure uses: Azure/login@e15b166166a8746d1a47596803bd8c1b595455cf # v1.6.0 @@ -228,7 +228,7 @@ jobs: _RELEASE_TAG: ${{ needs.setup.outputs.tag-name }} steps: - name: Checkout Repo - uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7 + uses: actions/checkout@d632683dd7b4114ad314bca15554477dd762a938 # v4.2.0 - name: Print Environment run: | diff --git a/.github/workflows/publish-web.yml b/.github/workflows/publish-web.yml index 4409da93560..02a60ee4222 100644 --- a/.github/workflows/publish-web.yml +++ b/.github/workflows/publish-web.yml @@ -27,7 +27,7 @@ jobs: tag_version: ${{ steps.version.outputs.tag }} steps: - name: Checkout repo - uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7 + uses: actions/checkout@d632683dd7b4114ad314bca15554477dd762a938 # v4.2.0 - name: Branch check if: ${{ inputs.publish_type != 'Dry Run' }} @@ -67,7 +67,7 @@ jobs: echo "Github Release Option: $_RELEASE_OPTION" - name: Checkout repo - uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7 + uses: actions/checkout@d632683dd7b4114ad314bca15554477dd762a938 # v4.2.0 ########## ACR ########## - name: Login to Azure - PROD Subscription diff --git a/.github/workflows/release-browser.yml b/.github/workflows/release-browser.yml index 2811b23af9b..f190889414b 100644 --- a/.github/workflows/release-browser.yml +++ b/.github/workflows/release-browser.yml @@ -27,7 +27,7 @@ jobs: release-version: ${{ steps.version.outputs.version }} steps: - name: Checkout repo - uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7 + uses: actions/checkout@d632683dd7b4114ad314bca15554477dd762a938 # v4.2.0 - name: Branch check if: ${{ github.event.inputs.release_type != 'Dry Run' }} @@ -56,7 +56,7 @@ jobs: needs: setup steps: - name: Checkout repo - uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7 + uses: actions/checkout@d632683dd7b4114ad314bca15554477dd762a938 # v4.2.0 - name: Testing locales - extName length run: | diff --git a/.github/workflows/release-cli.yml b/.github/workflows/release-cli.yml index cd450b2cd79..8f5123b50b2 100644 --- a/.github/workflows/release-cli.yml +++ b/.github/workflows/release-cli.yml @@ -27,7 +27,7 @@ jobs: release-version: ${{ steps.version.outputs.version }} steps: - name: Checkout repo - uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7 + uses: actions/checkout@d632683dd7b4114ad314bca15554477dd762a938 # v4.2.0 - name: Branch check if: ${{ inputs.release_type != 'Dry Run' }} diff --git a/.github/workflows/release-desktop-beta.yml b/.github/workflows/release-desktop-beta.yml index 80b1ae092c6..23b06d71dd3 100644 --- a/.github/workflows/release-desktop-beta.yml +++ b/.github/workflows/release-desktop-beta.yml @@ -24,7 +24,7 @@ jobs: node_version: ${{ steps.retrieve-node-version.outputs.node_version }} steps: - name: Checkout repo - uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7 + uses: actions/checkout@d632683dd7b4114ad314bca15554477dd762a938 # v4.2.0 - name: Branch check run: | @@ -125,7 +125,7 @@ jobs: working-directory: apps/desktop steps: - name: Checkout repo - uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7 + uses: actions/checkout@d632683dd7b4114ad314bca15554477dd762a938 # v4.2.0 with: ref: ${{ needs.setup.outputs.branch-name }} @@ -215,7 +215,7 @@ jobs: NODE_OPTIONS: --max_old_space_size=4096 steps: - name: Checkout repo - uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7 + uses: actions/checkout@d632683dd7b4114ad314bca15554477dd762a938 # v4.2.0 with: ref: ${{ needs.setup.outputs.branch-name }} @@ -404,7 +404,7 @@ jobs: working-directory: apps/desktop steps: - name: Checkout repo - uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7 + uses: actions/checkout@d632683dd7b4114ad314bca15554477dd762a938 # v4.2.0 with: ref: ${{ needs.setup.outputs.branch-name }} @@ -538,7 +538,7 @@ jobs: working-directory: apps/desktop steps: - name: Checkout repo - uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7 + uses: actions/checkout@d632683dd7b4114ad314bca15554477dd762a938 # v4.2.0 with: ref: ${{ needs.setup.outputs.branch-name }} @@ -751,7 +751,7 @@ jobs: working-directory: apps/desktop steps: - name: Checkout repo - uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7 + uses: actions/checkout@d632683dd7b4114ad314bca15554477dd762a938 # v4.2.0 with: ref: ${{ needs.setup.outputs.branch-name }} @@ -1011,7 +1011,7 @@ jobs: - release steps: - name: Checkout repo - uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7 + uses: actions/checkout@d632683dd7b4114ad314bca15554477dd762a938 # v4.2.0 - name: Setup git config run: | diff --git a/.github/workflows/release-desktop.yml b/.github/workflows/release-desktop.yml index 5b75460ef92..d69c15559b3 100644 --- a/.github/workflows/release-desktop.yml +++ b/.github/workflows/release-desktop.yml @@ -27,7 +27,7 @@ jobs: release-channel: ${{ steps.release-channel.outputs.channel }} steps: - name: Checkout repo - uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7 + uses: actions/checkout@d632683dd7b4114ad314bca15554477dd762a938 # v4.2.0 - name: Branch check if: ${{ github.event.inputs.release_type != 'Dry Run' }} diff --git a/.github/workflows/release-web.yml b/.github/workflows/release-web.yml index 982e3867585..92f5701889b 100644 --- a/.github/workflows/release-web.yml +++ b/.github/workflows/release-web.yml @@ -24,7 +24,7 @@ jobs: tag_version: ${{ steps.version.outputs.tag }} steps: - name: Checkout repo - uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7 + uses: actions/checkout@d632683dd7b4114ad314bca15554477dd762a938 # v4.2.0 - name: Branch check if: ${{ github.event.inputs.release_type != 'Dry Run' }} diff --git a/.github/workflows/scan.yml b/.github/workflows/scan.yml index 2320bae72f3..f15a14dd117 100644 --- a/.github/workflows/scan.yml +++ b/.github/workflows/scan.yml @@ -27,7 +27,7 @@ jobs: steps: - name: Check out repo - uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7 + uses: actions/checkout@d632683dd7b4114ad314bca15554477dd762a938 # v4.2.0 with: ref: ${{ github.event.pull_request.head.sha }} @@ -47,7 +47,7 @@ jobs: --output-path . ${{ env.INCREMENTAL }} - name: Upload Checkmarx results to GitHub - uses: github/codeql-action/upload-sarif@294a9d92911152fe08befb9ec03e240add280cb3 # v3.26.8 + uses: github/codeql-action/upload-sarif@461ef6c76dfe95d5c364de2f431ddbd31a417628 # v3.26.9 with: sarif_file: cx_result.sarif @@ -61,7 +61,7 @@ jobs: steps: - name: Check out repo - uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7 + uses: actions/checkout@d632683dd7b4114ad314bca15554477dd762a938 # v4.2.0 with: fetch-depth: 0 ref: ${{ github.event.pull_request.head.sha }} diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index fbe0c0a798b..fd378213c9f 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -41,7 +41,7 @@ jobs: steps: - name: Check out repo - uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7 + uses: actions/checkout@d632683dd7b4114ad314bca15554477dd762a938 # v4.2.0 - name: Get Node Version id: retrieve-node-version @@ -121,7 +121,7 @@ jobs: sudo apt-get install -y gnome-keyring dbus-x11 - name: Check out repo - uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7 + uses: actions/checkout@d632683dd7b4114ad314bca15554477dd762a938 # v4.2.0 - name: Build working-directory: ./apps/desktop/desktop_native diff --git a/.github/workflows/version-bump.yml b/.github/workflows/version-bump.yml index fea68161bab..d18097ee1c2 100644 --- a/.github/workflows/version-bump.yml +++ b/.github/workflows/version-bump.yml @@ -58,7 +58,7 @@ jobs: fi - name: Checkout Branch - uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7 + uses: actions/checkout@d632683dd7b4114ad314bca15554477dd762a938 # v4.2.0 with: ref: main @@ -533,7 +533,7 @@ jobs: runs-on: ubuntu-22.04 steps: - name: Checkout Branch - uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7 + uses: actions/checkout@d632683dd7b4114ad314bca15554477dd762a938 # v4.2.0 with: ref: main diff --git a/apps/browser/src/_locales/en/messages.json b/apps/browser/src/_locales/en/messages.json index f7db6d78f21..ec0fac137df 100644 --- a/apps/browser/src/_locales/en/messages.json +++ b/apps/browser/src/_locales/en/messages.json @@ -604,6 +604,15 @@ "yourVaultIsLocked": { "message": "Your vault is locked. Verify your identity to continue." }, + "yourVaultIsLockedV2": { + "message": "Your vault is locked" + }, + "yourAccountIsLocked": { + "message": "Your account is locked" + }, + "or": { + "message": "or" + }, "unlock": { "message": "Unlock" }, @@ -1936,6 +1945,9 @@ "unlockWithBiometrics": { "message": "Unlock with biometrics" }, + "unlockWithMasterPassword": { + "message": "Unlock with master password" + }, "awaitDesktop": { "message": "Awaiting confirmation from desktop" }, @@ -2509,7 +2521,7 @@ "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, "sendFilePopoutDialogDesc": { - "message": "To create a file Send, you need to pop out te extension to a new window.", + "message": "To create a file Send, you need to pop out the extension to a new window.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, "sendLinuxChromiumFileWarning": { @@ -3623,6 +3635,9 @@ "typePasskey": { "message": "Passkey" }, + "accessing": { + "message": "Accessing" + }, "passkeyNotCopied": { "message": "Passkey will not be copied" }, diff --git a/apps/browser/src/auth/popup/account-switching/current-account.component.ts b/apps/browser/src/auth/popup/account-switching/current-account.component.ts index 6c7c1e7d92f..12210b2b452 100644 --- a/apps/browser/src/auth/popup/account-switching/current-account.component.ts +++ b/apps/browser/src/auth/popup/account-switching/current-account.component.ts @@ -59,7 +59,7 @@ export class CurrentAccountComponent { } async currentAccountClicked() { - if (this.route.snapshot.data.state.includes("account-switcher")) { + if (this.route.snapshot.data?.state?.includes("account-switcher")) { this.location.back(); } else { await this.router.navigate(["/account-switcher"]); diff --git a/apps/browser/src/auth/popup/lock.component.ts b/apps/browser/src/auth/popup/lock.component.ts index f5413e4bea4..96bda7012d1 100644 --- a/apps/browser/src/auth/popup/lock.component.ts +++ b/apps/browser/src/auth/popup/lock.component.ts @@ -23,11 +23,10 @@ import { LogService } from "@bitwarden/common/platform/abstractions/log.service" import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service"; import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; import { StateService } from "@bitwarden/common/platform/abstractions/state.service"; -import { BiometricStateService } from "@bitwarden/common/platform/biometrics/biometric-state.service"; -import { BiometricsService } from "@bitwarden/common/platform/biometrics/biometric.service"; import { PasswordStrengthServiceAbstraction } from "@bitwarden/common/tools/password-strength"; import { SyncService } from "@bitwarden/common/vault/abstractions/sync/sync.service.abstraction"; import { DialogService, ToastService } from "@bitwarden/components"; +import { BiometricsService, BiometricStateService } from "@bitwarden/key-management"; import { BiometricErrors, BiometricErrorTypes } from "../../models/biometricErrors"; import { BrowserRouterService } from "../../platform/popup/services/browser-router.service"; diff --git a/apps/browser/src/auth/popup/settings/account-security-v1.component.ts b/apps/browser/src/auth/popup/settings/account-security-v1.component.ts index 4975ba5f7a2..d2a515b2599 100644 --- a/apps/browser/src/auth/popup/settings/account-security-v1.component.ts +++ b/apps/browser/src/auth/popup/settings/account-security-v1.component.ts @@ -31,14 +31,13 @@ import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.servic import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service"; import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; import { StateService } from "@bitwarden/common/platform/abstractions/state.service"; -import { BiometricStateService } from "@bitwarden/common/platform/biometrics/biometric-state.service"; -import { BiometricsService } from "@bitwarden/common/platform/biometrics/biometric.service"; import { VaultTimeout, VaultTimeoutOption, VaultTimeoutStringType, } from "@bitwarden/common/types/vault-timeout.type"; import { DialogService } from "@bitwarden/components"; +import { BiometricStateService, BiometricsService } from "@bitwarden/key-management"; import { BiometricErrors, BiometricErrorTypes } from "../../../models/biometricErrors"; import { BrowserApi } from "../../../platform/browser/browser-api"; diff --git a/apps/browser/src/auth/popup/settings/account-security.component.ts b/apps/browser/src/auth/popup/settings/account-security.component.ts index 8e0acc7d641..c546db3c97e 100644 --- a/apps/browser/src/auth/popup/settings/account-security.component.ts +++ b/apps/browser/src/auth/popup/settings/account-security.component.ts @@ -33,8 +33,6 @@ import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.servic import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service"; import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; import { StateService } from "@bitwarden/common/platform/abstractions/state.service"; -import { BiometricStateService } from "@bitwarden/common/platform/biometrics/biometric-state.service"; -import { BiometricsService } from "@bitwarden/common/platform/biometrics/biometric.service"; import { VaultTimeout, VaultTimeoutOption, @@ -54,6 +52,7 @@ import { TypographyModule, ToastService, } from "@bitwarden/components"; +import { BiometricsService, BiometricStateService } from "@bitwarden/key-management"; import { BiometricErrors, BiometricErrorTypes } from "../../../models/biometricErrors"; import { BrowserApi } from "../../../platform/browser/browser-api"; diff --git a/apps/browser/src/autofill/fido2/content/fido2-page-script-append.mv2.spec.ts b/apps/browser/src/autofill/fido2/content/fido2-page-script-append.mv2.spec.ts index f5f8dd770c7..6b9b41b5aac 100644 --- a/apps/browser/src/autofill/fido2/content/fido2-page-script-append.mv2.spec.ts +++ b/apps/browser/src/autofill/fido2/content/fido2-page-script-append.mv2.spec.ts @@ -57,19 +57,4 @@ describe("FIDO2 page-script for manifest v2", () => { ); expect(createdScriptElement.src).toBe(`chrome-extension://id/${Fido2ContentScript.PageScript}`); }); - - it("removes the appended `page-script.js` file after the script has triggered a load event", () => { - createdScriptElement = document.createElement("script"); - jest.spyOn(window.document, "createElement").mockImplementation((element) => { - return createdScriptElement; - }); - - require("./fido2-page-script-append.mv2"); - - jest.spyOn(createdScriptElement, "remove"); - createdScriptElement.dispatchEvent(new Event("load")); - jest.runAllTimers(); - - expect(createdScriptElement.remove).toHaveBeenCalled(); - }); }); diff --git a/apps/browser/src/autofill/fido2/content/fido2-page-script-append.mv2.ts b/apps/browser/src/autofill/fido2/content/fido2-page-script-append.mv2.ts index e5280c088bc..dd5f33dffb0 100644 --- a/apps/browser/src/autofill/fido2/content/fido2-page-script-append.mv2.ts +++ b/apps/browser/src/autofill/fido2/content/fido2-page-script-append.mv2.ts @@ -9,13 +9,8 @@ const script = globalContext.document.createElement("script"); script.src = chrome.runtime.getURL("content/fido2-page-script.js"); - script.addEventListener("load", removeScriptOnLoad); const scriptInsertionPoint = globalContext.document.head || globalContext.document.documentElement; scriptInsertionPoint.prepend(script); - - function removeScriptOnLoad() { - globalThis.setTimeout(() => script?.remove(), 5000); - } })(globalThis); diff --git a/apps/browser/src/autofill/fido2/content/fido2-page-script-delay-append.mv2.ts b/apps/browser/src/autofill/fido2/content/fido2-page-script-delay-append.mv2.ts index c75a37c1b65..2ada31fdfe2 100644 --- a/apps/browser/src/autofill/fido2/content/fido2-page-script-delay-append.mv2.ts +++ b/apps/browser/src/autofill/fido2/content/fido2-page-script-delay-append.mv2.ts @@ -9,7 +9,6 @@ const script = globalContext.document.createElement("script"); script.src = chrome.runtime.getURL("content/fido2-page-script.js"); - script.addEventListener("load", removeScriptOnLoad); // We are ensuring that the script injection is delayed in the event that we are loading // within an iframe element. This prevents an issue with web mail clients that load content @@ -29,8 +28,4 @@ globalContext.document.head || globalContext.document.documentElement; scriptInsertionPoint.prepend(script); } - - function removeScriptOnLoad() { - globalThis.setTimeout(() => script?.remove(), 5000); - } })(globalThis); diff --git a/apps/browser/src/autofill/services/autofill.service.ts b/apps/browser/src/autofill/services/autofill.service.ts index cd49da72194..de79fd974ef 100644 --- a/apps/browser/src/autofill/services/autofill.service.ts +++ b/apps/browser/src/autofill/services/autofill.service.ts @@ -237,7 +237,9 @@ export default class AutofillService implements AutofillServiceInterface { FeatureFlag.InlineMenuPositioningImprovements, ); if (!inlineMenuPositioningImprovements) { - return "bootstrap-legacy-autofill-overlay.js"; + return !inlineMenuVisibility + ? "bootstrap-autofill.js" + : "bootstrap-legacy-autofill-overlay.js"; } const enableChangedPasswordPrompt = await firstValueFrom( diff --git a/apps/browser/src/background/main.background.ts b/apps/browser/src/background/main.background.ts index 43a6d5968e9..62f9bf05dad 100644 --- a/apps/browser/src/background/main.background.ts +++ b/apps/browser/src/background/main.background.ts @@ -95,11 +95,6 @@ import { ObservableStorageService, } from "@bitwarden/common/platform/abstractions/storage.service"; import { SystemService as SystemServiceAbstraction } from "@bitwarden/common/platform/abstractions/system.service"; -import { - BiometricStateService, - DefaultBiometricStateService, -} from "@bitwarden/common/platform/biometrics/biometric-state.service"; -import { BiometricsService } from "@bitwarden/common/platform/biometrics/biometric.service"; import { StateFactory } from "@bitwarden/common/platform/factories/state-factory"; import { Message, MessageListener, MessageSender } from "@bitwarden/common/platform/messaging"; // eslint-disable-next-line no-restricted-imports -- Used for dependency creation @@ -197,6 +192,11 @@ import { ImportService, ImportServiceAbstraction, } from "@bitwarden/importer/core"; +import { + BiometricStateService, + DefaultBiometricStateService, + BiometricsService, +} from "@bitwarden/key-management"; import { IndividualVaultExportService, IndividualVaultExportServiceAbstraction, @@ -225,6 +225,7 @@ import { BrowserFido2UserInterfaceService } from "../autofill/fido2/services/bro import { AutofillService as AutofillServiceAbstraction } from "../autofill/services/abstractions/autofill.service"; import AutofillService from "../autofill/services/autofill.service"; import { SafariApp } from "../browser/safariApp"; +import { BackgroundBrowserBiometricsService } from "../key-management/biometrics/background-browser-biometrics.service"; import { BrowserApi } from "../platform/browser/browser-api"; import { UpdateBadge } from "../platform/listeners/update-badge"; /* eslint-disable no-restricted-imports */ @@ -233,7 +234,6 @@ import { ChromeMessageSender } from "../platform/messaging/chrome-message.sender import { OffscreenDocumentService } from "../platform/offscreen-document/abstractions/offscreen-document"; import { DefaultOffscreenDocumentService } from "../platform/offscreen-document/offscreen-document.service"; import { BrowserTaskSchedulerService } from "../platform/services/abstractions/browser-task-scheduler.service"; -import { BackgroundBrowserBiometricsService } from "../platform/services/background-browser-biometrics.service"; import { BrowserCryptoService } from "../platform/services/browser-crypto.service"; import { BrowserEnvironmentService } from "../platform/services/browser-environment.service"; import BrowserLocalStorageService from "../platform/services/browser-local-storage.service"; @@ -753,6 +753,7 @@ export default class MainBackground { this.accountService, this.masterPasswordService, this.cryptoService, + this.encryptService, this.apiService, this.stateProvider, ); diff --git a/apps/browser/src/background/nativeMessaging.background.ts b/apps/browser/src/background/nativeMessaging.background.ts index 68a43fbdfe3..d483b10bc38 100644 --- a/apps/browser/src/background/nativeMessaging.background.ts +++ b/apps/browser/src/background/nativeMessaging.background.ts @@ -10,11 +10,11 @@ import { EncryptService } from "@bitwarden/common/platform/abstractions/encrypt. import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service"; import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; -import { BiometricStateService } from "@bitwarden/common/platform/biometrics/biometric-state.service"; import { Utils } from "@bitwarden/common/platform/misc/utils"; import { EncString } from "@bitwarden/common/platform/models/domain/enc-string"; import { SymmetricCryptoKey } from "@bitwarden/common/platform/models/domain/symmetric-crypto-key"; import { UserKey } from "@bitwarden/common/types/key"; +import { BiometricStateService } from "@bitwarden/key-management"; import { BrowserApi } from "../platform/browser/browser-api"; diff --git a/apps/browser/src/platform/services/background-browser-biometrics.service.ts b/apps/browser/src/key-management/biometrics/background-browser-biometrics.service.ts similarity index 100% rename from apps/browser/src/platform/services/background-browser-biometrics.service.ts rename to apps/browser/src/key-management/biometrics/background-browser-biometrics.service.ts diff --git a/apps/browser/src/platform/services/browser-biometrics.service.ts b/apps/browser/src/key-management/biometrics/browser-biometrics.service.ts similarity index 76% rename from apps/browser/src/platform/services/browser-biometrics.service.ts rename to apps/browser/src/key-management/biometrics/browser-biometrics.service.ts index 84734fb4927..7ffbed45415 100644 --- a/apps/browser/src/platform/services/browser-biometrics.service.ts +++ b/apps/browser/src/key-management/biometrics/browser-biometrics.service.ts @@ -1,8 +1,8 @@ import { Injectable } from "@angular/core"; -import { BiometricsService } from "@bitwarden/common/platform/biometrics/biometric.service"; +import { BiometricsService } from "@bitwarden/key-management"; -import { BrowserApi } from "../browser/browser-api"; +import { BrowserApi } from "../../platform/browser/browser-api"; @Injectable() export abstract class BrowserBiometricsService extends BiometricsService { diff --git a/apps/browser/src/platform/services/foreground-browser-biometrics.ts b/apps/browser/src/key-management/biometrics/foreground-browser-biometrics.ts similarity index 93% rename from apps/browser/src/platform/services/foreground-browser-biometrics.ts rename to apps/browser/src/key-management/biometrics/foreground-browser-biometrics.ts index ee55de20108..f50468c8b7a 100644 --- a/apps/browser/src/platform/services/foreground-browser-biometrics.ts +++ b/apps/browser/src/key-management/biometrics/foreground-browser-biometrics.ts @@ -1,4 +1,4 @@ -import { BrowserApi } from "../browser/browser-api"; +import { BrowserApi } from "../../platform/browser/browser-api"; import { BrowserBiometricsService } from "./browser-biometrics.service"; diff --git a/apps/browser/src/platform/browser/browser-api.register-content-scripts-polyfill.ts b/apps/browser/src/platform/browser/browser-api.register-content-scripts-polyfill.ts index 8a20f3e9997..b0dc60ed126 100644 --- a/apps/browser/src/platform/browser/browser-api.register-content-scripts-polyfill.ts +++ b/apps/browser/src/platform/browser/browser-api.register-content-scripts-polyfill.ts @@ -43,17 +43,23 @@ function buildRegisterContentScriptsPolyfill() { function NestedProxy(target: T): T { return new Proxy(target, { get(target, prop) { - if (!target[prop as keyof T]) { + const propertyValue = target[prop as keyof T]; + + if (!propertyValue) { return; } - if (typeof target[prop as keyof T] !== "function") { - return NestedProxy(target[prop as keyof T]); + if (typeof propertyValue === "object") { + return NestedProxy(propertyValue); + } + + if (typeof propertyValue !== "function") { + return propertyValue; } return (...arguments_: any[]) => new Promise((resolve, reject) => { - target[prop as keyof T](...arguments_, (result: any) => { + propertyValue(...arguments_, (result: any) => { if (chrome.runtime.lastError) { reject(new Error(chrome.runtime.lastError.message)); } else { diff --git a/apps/browser/src/platform/popup/layout/popup-tab-navigation.component.html b/apps/browser/src/platform/popup/layout/popup-tab-navigation.component.html index a0ff252c6c2..972a60d31ad 100644 --- a/apps/browser/src/platform/popup/layout/popup-tab-navigation.component.html +++ b/apps/browser/src/platform/popup/layout/popup-tab-navigation.component.html @@ -6,7 +6,7 @@
policyAppliesToActiveUser), switchMap(() => this.sendService.sends$), - map((sends) => sends.length > 1), + map((sends) => sends.length > 0), takeUntilDestroyed(), ) .subscribe((hasSends) => { diff --git a/apps/browser/src/platform/services/browser-crypto.service.ts b/apps/browser/src/platform/services/browser-crypto.service.ts index 1d61fb4c8ed..d0b4e3add7f 100644 --- a/apps/browser/src/platform/services/browser-crypto.service.ts +++ b/apps/browser/src/platform/services/browser-crypto.service.ts @@ -10,14 +10,13 @@ import { KeyGenerationService } from "@bitwarden/common/platform/abstractions/ke import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; import { StateService } from "@bitwarden/common/platform/abstractions/state.service"; -import { BiometricStateService } from "@bitwarden/common/platform/biometrics/biometric-state.service"; -import { BiometricsService } from "@bitwarden/common/platform/biometrics/biometric.service"; import { KeySuffixOptions } from "@bitwarden/common/platform/enums"; import { CryptoService } from "@bitwarden/common/platform/services/crypto.service"; import { USER_KEY } from "@bitwarden/common/platform/services/key-state/user-key.state"; import { StateProvider } from "@bitwarden/common/platform/state"; import { UserId } from "@bitwarden/common/types/guid"; import { UserKey } from "@bitwarden/common/types/key"; +import { BiometricsService, BiometricStateService } from "@bitwarden/key-management"; export class BrowserCryptoService extends CryptoService { constructor( diff --git a/apps/browser/src/popup/app-routing.module.ts b/apps/browser/src/popup/app-routing.module.ts index 14f35a78fb3..9fd52470c0a 100644 --- a/apps/browser/src/popup/app-routing.module.ts +++ b/apps/browser/src/popup/app-routing.module.ts @@ -17,6 +17,8 @@ import { extensionRefreshSwap } from "@bitwarden/angular/utils/extension-refresh import { AnonLayoutWrapperComponent, AnonLayoutWrapperData, + LockIcon, + LockV2Component, PasswordHintComponent, RegistrationFinishComponent, RegistrationStartComponent, @@ -31,7 +33,10 @@ import { twofactorRefactorSwap } from "../../../../libs/angular/src/utils/two-fa import { fido2AuthGuard } from "../auth/guards/fido2-auth.guard"; import { AccountSwitcherComponent } from "../auth/popup/account-switching/account-switcher.component"; import { EnvironmentComponent } from "../auth/popup/environment.component"; -import { ExtensionAnonLayoutWrapperComponent } from "../auth/popup/extension-anon-layout-wrapper/extension-anon-layout-wrapper.component"; +import { + ExtensionAnonLayoutWrapperComponent, + ExtensionAnonLayoutWrapperData, +} from "../auth/popup/extension-anon-layout-wrapper/extension-anon-layout-wrapper.component"; import { HintComponent } from "../auth/popup/hint.component"; import { HomeComponent } from "../auth/popup/home.component"; import { LockComponent } from "../auth/popup/lock.component"; @@ -109,6 +114,21 @@ import { debounceNavigationGuard } from "./services/debounce-navigation.service" import { TabsV2Component } from "./tabs-v2.component"; import { TabsComponent } from "./tabs.component"; +/** + * Data properties acceptable for use in extension route objects + */ +export interface RouteDataProperties { + /** + * A state string which identifies the current route for the sake of transition animation logic. + * The state string is passed into [@routerTransition] in the app.component. + */ + state: string; + /** + * A boolean to indicate that the URL should not be saved in memory in the BrowserRouterSvc. + */ + doNotSaveUrl?: boolean; +} + const unauthRouteOverrides = { homepage: () => { return BrowserPopupUtils.inPopout(window) ? "/tabs/vault" : "/tabs/current"; @@ -134,36 +154,37 @@ const routes: Routes = [ path: "home", component: HomeComponent, canActivate: [unauthGuardFn(unauthRouteOverrides)], - data: { state: "home" }, + data: { state: "home" } satisfies RouteDataProperties, }, ...extensionRefreshSwap(Fido2V1Component, Fido2Component, { path: "fido2", canActivate: [fido2AuthGuard], - data: { state: "fido2" }, + data: { state: "fido2" } satisfies RouteDataProperties, }), { path: "login", component: LoginComponent, canActivate: [unauthGuardFn(unauthRouteOverrides)], - data: { state: "login" }, + data: { state: "login" } satisfies RouteDataProperties, }, { path: "login-with-device", component: LoginViaAuthRequestComponent, canActivate: [], - data: { state: "login-with-device" }, + data: { state: "login-with-device" } satisfies RouteDataProperties, }, { path: "admin-approval-requested", component: LoginViaAuthRequestComponent, canActivate: [], - data: { state: "login-with-device" }, + data: { state: "login-with-device" } satisfies RouteDataProperties, }, { path: "lock", component: LockComponent, canActivate: [lockGuard()], - data: { state: "lock", doNotSaveUrl: true }, + canMatch: [extensionRefreshRedirect("/lockV2")], + data: { state: "lock", doNotSaveUrl: true } satisfies RouteDataProperties, }, ...twofactorRefactorSwap( TwoFactorComponent, @@ -171,12 +192,12 @@ const routes: Routes = [ { path: "2fa", canActivate: [unauthGuardFn(unauthRouteOverrides)], - data: { state: "2fa" }, + data: { state: "2fa" } satisfies RouteDataProperties, }, { path: "2fa", canActivate: [unauthGuardFn(unauthRouteOverrides)], - data: { state: "2fa" }, + data: { state: "2fa" } satisfies RouteDataProperties, children: [ { path: "", @@ -189,200 +210,201 @@ const routes: Routes = [ path: "2fa-options", component: TwoFactorOptionsComponent, canActivate: [unauthGuardFn(unauthRouteOverrides)], - data: { state: "2fa-options" }, + data: { state: "2fa-options" } satisfies RouteDataProperties, }, { path: "login-initiated", component: LoginDecryptionOptionsComponent, canActivate: [tdeDecryptionRequiredGuard()], + data: { state: "login-initiated" } satisfies RouteDataProperties, }, { path: "sso", component: SsoComponent, canActivate: [unauthGuardFn(unauthRouteOverrides)], - data: { state: "sso" }, + data: { state: "sso" } satisfies RouteDataProperties, }, { path: "set-password", component: SetPasswordComponent, - data: { state: "set-password" }, + data: { state: "set-password" } satisfies RouteDataProperties, }, { path: "remove-password", component: RemovePasswordComponent, canActivate: [authGuard], - data: { state: "remove-password" }, + data: { state: "remove-password" } satisfies RouteDataProperties, }, { path: "register", component: RegisterComponent, canActivate: [unauthGuardFn(unauthRouteOverrides)], - data: { state: "register" }, + data: { state: "register" } satisfies RouteDataProperties, }, { path: "environment", component: EnvironmentComponent, canActivate: [unauthGuardFn(unauthRouteOverrides)], - data: { state: "environment" }, + data: { state: "environment" } satisfies RouteDataProperties, }, { path: "ciphers", component: VaultItemsComponent, canActivate: [authGuard], - data: { state: "ciphers" }, + data: { state: "ciphers" } satisfies RouteDataProperties, }, ...extensionRefreshSwap(ViewComponent, ViewV2Component, { path: "view-cipher", canActivate: [authGuard], - data: { state: "view-cipher" }, + data: { state: "view-cipher" } satisfies RouteDataProperties, }), { path: "cipher-password-history", component: PasswordHistoryComponent, canActivate: [authGuard], - data: { state: "cipher-password-history" }, + data: { state: "cipher-password-history" } satisfies RouteDataProperties, }, ...extensionRefreshSwap(AddEditComponent, AddEditV2Component, { path: "add-cipher", canActivate: [authGuard, debounceNavigationGuard()], - data: { state: "add-cipher" }, + data: { state: "add-cipher" } satisfies RouteDataProperties, runGuardsAndResolvers: "always", }), ...extensionRefreshSwap(AddEditComponent, AddEditV2Component, { path: "edit-cipher", canActivate: [authGuard, debounceNavigationGuard()], - data: { state: "edit-cipher" }, + data: { state: "edit-cipher" } satisfies RouteDataProperties, runGuardsAndResolvers: "always", }), { path: "share-cipher", component: ShareComponent, canActivate: [authGuard], - data: { state: "share-cipher" }, + data: { state: "share-cipher" } satisfies RouteDataProperties, }, { path: "collections", component: CollectionsComponent, canActivate: [authGuard], - data: { state: "collections" }, + data: { state: "collections" } satisfies RouteDataProperties, }, ...extensionRefreshSwap(AttachmentsComponent, AttachmentsV2Component, { path: "attachments", canActivate: [authGuard], - data: { state: "attachments" }, + data: { state: "attachments" } satisfies RouteDataProperties, }), { path: "generator", component: GeneratorComponent, canActivate: [authGuard], - data: { state: "generator" }, + data: { state: "generator" } satisfies RouteDataProperties, }, ...extensionRefreshSwap(PasswordGeneratorHistoryComponent, CredentialGeneratorHistoryComponent, { path: "generator-history", canActivate: [authGuard], - data: { state: "generator-history" }, + data: { state: "generator-history" } satisfies RouteDataProperties, }), ...extensionRefreshSwap(ImportBrowserComponent, ImportBrowserV2Component, { path: "import", canActivate: [authGuard], - data: { state: "import" }, + data: { state: "import" } satisfies RouteDataProperties, }), ...extensionRefreshSwap(ExportBrowserComponent, ExportBrowserV2Component, { path: "export", canActivate: [authGuard], - data: { state: "export" }, + data: { state: "export" } satisfies RouteDataProperties, }), ...extensionRefreshSwap(AutofillV1Component, AutofillComponent, { path: "autofill", canActivate: [authGuard], - data: { state: "autofill" }, + data: { state: "autofill" } satisfies RouteDataProperties, }), ...extensionRefreshSwap(AccountSecurityV1Component, AccountSecurityComponent, { path: "account-security", canActivate: [authGuard], - data: { state: "account-security" }, + data: { state: "account-security" } satisfies RouteDataProperties, }), ...extensionRefreshSwap(NotificationsSettingsV1Component, NotificationsSettingsComponent, { path: "notifications", canActivate: [authGuard], - data: { state: "notifications" }, + data: { state: "notifications" } satisfies RouteDataProperties, }), ...extensionRefreshSwap(VaultSettingsComponent, VaultSettingsV2Component, { path: "vault-settings", canActivate: [authGuard], - data: { state: "vault-settings" }, + data: { state: "vault-settings" } satisfies RouteDataProperties, }), ...extensionRefreshSwap(FoldersComponent, FoldersV2Component, { path: "folders", canActivate: [authGuard], - data: { state: "folders" }, + data: { state: "folders" } satisfies RouteDataProperties, }), { path: "add-folder", component: FolderAddEditComponent, canActivate: [authGuard], - data: { state: "add-folder" }, + data: { state: "add-folder" } satisfies RouteDataProperties, }, { path: "edit-folder", component: FolderAddEditComponent, canActivate: [authGuard], - data: { state: "edit-folder" }, + data: { state: "edit-folder" } satisfies RouteDataProperties, }, { path: "sync", component: SyncComponent, canActivate: [authGuard], - data: { state: "sync" }, + data: { state: "sync" } satisfies RouteDataProperties, }, ...extensionRefreshSwap(ExcludedDomainsV1Component, ExcludedDomainsComponent, { path: "excluded-domains", canActivate: [authGuard], - data: { state: "excluded-domains" }, + data: { state: "excluded-domains" } satisfies RouteDataProperties, }), ...extensionRefreshSwap(PremiumComponent, PremiumV2Component, { path: "premium", component: PremiumComponent, canActivate: [authGuard], - data: { state: "premium" }, + data: { state: "premium" } satisfies RouteDataProperties, }), ...extensionRefreshSwap(AppearanceComponent, AppearanceV2Component, { path: "appearance", canActivate: [authGuard], - data: { state: "appearance" }, + data: { state: "appearance" } satisfies RouteDataProperties, }), ...extensionRefreshSwap(AddEditComponent, AddEditV2Component, { path: "clone-cipher", canActivate: [authGuard], - data: { state: "clone-cipher" }, + data: { state: "clone-cipher" } satisfies RouteDataProperties, }), { path: "send-type", component: SendTypeComponent, canActivate: [authGuard], - data: { state: "send-type" }, + data: { state: "send-type" } satisfies RouteDataProperties, }, ...extensionRefreshSwap(SendAddEditComponent, SendAddEditV2Component, { path: "add-send", canActivate: [authGuard], - data: { state: "add-send" }, + data: { state: "add-send" } satisfies RouteDataProperties, }), ...extensionRefreshSwap(SendAddEditComponent, SendAddEditV2Component, { path: "edit-send", canActivate: [authGuard], - data: { state: "edit-send" }, + data: { state: "edit-send" } satisfies RouteDataProperties, }), { path: "send-created", component: SendCreatedComponent, canActivate: [authGuard], - data: { state: "send" }, + data: { state: "send" } satisfies RouteDataProperties, }, { path: "update-temp-password", component: UpdateTempPasswordComponent, canActivate: [authGuard], - data: { state: "update-temp-password" }, + data: { state: "update-temp-password" } satisfies RouteDataProperties, }, ...unauthUiRefreshSwap( HintComponent, @@ -392,7 +414,7 @@ const routes: Routes = [ canActivate: [unauthGuardFn(unauthRouteOverrides)], data: { state: "hint", - }, + } satisfies RouteDataProperties, }, { path: "", @@ -406,7 +428,7 @@ const routes: Routes = [ pageIcon: UserLockIcon, showBackButton: true, state: "hint", - }, + } satisfies RouteDataProperties & ExtensionAnonLayoutWrapperData, children: [ { path: "", component: PasswordHintComponent }, { @@ -419,6 +441,28 @@ const routes: Routes = [ ], }, ), + { + path: "", + component: ExtensionAnonLayoutWrapperComponent, + children: [ + { + path: "lockV2", + canActivate: [canAccessFeature(FeatureFlag.ExtensionRefresh), lockGuard()], + data: { + pageIcon: LockIcon, + pageTitle: "yourVaultIsLockedV2", + showReadonlyHostname: true, + showAcctSwitcher: true, + } satisfies ExtensionAnonLayoutWrapperData, + children: [ + { + path: "", + component: LockV2Component, + }, + ], + }, + ], + }, { path: "", component: AnonLayoutWrapperComponent, @@ -426,7 +470,10 @@ const routes: Routes = [ { path: "signup", canActivate: [canAccessFeature(FeatureFlag.EmailVerification), unauthGuardFn()], - data: { pageTitle: "createAccount" } satisfies AnonLayoutWrapperData, + data: { + state: "signup", + pageTitle: "createAccount", + } satisfies RouteDataProperties & AnonLayoutWrapperData, children: [ { path: "", @@ -448,7 +495,8 @@ const routes: Routes = [ data: { pageTitle: "setAStrongPassword", pageSubtitle: "finishCreatingYourAccountBySettingAPassword", - } satisfies AnonLayoutWrapperData, + state: "finish-signup", + } satisfies RouteDataProperties & AnonLayoutWrapperData, children: [ { path: "", @@ -463,7 +511,8 @@ const routes: Routes = [ data: { pageTitle: "joinOrganization", pageSubtitle: "finishJoiningThisOrganizationBySettingAMasterPassword", - } satisfies AnonLayoutWrapperData, + state: "set-password-jit", + } satisfies RouteDataProperties & AnonLayoutWrapperData, }, ], }, @@ -471,21 +520,21 @@ const routes: Routes = [ path: "assign-collections", component: AssignCollections, canActivate: [canAccessFeature(FeatureFlag.ExtensionRefresh, true, "/")], - data: { state: "assign-collections" }, + data: { state: "assign-collections" } satisfies RouteDataProperties, }, ...extensionRefreshSwap(AboutPageComponent, AboutPageV2Component, { path: "about", canActivate: [authGuard], - data: { state: "about" }, + data: { state: "about" } satisfies RouteDataProperties, }), ...extensionRefreshSwap(MoreFromBitwardenPageComponent, MoreFromBitwardenPageV2Component, { path: "more-from-bitwarden", canActivate: [authGuard], - data: { state: "moreFromBitwarden" }, + data: { state: "moreFromBitwarden" } satisfies RouteDataProperties, }), ...extensionRefreshSwap(TabsComponent, TabsV2Component, { path: "tabs", - data: { state: "tabs" }, + data: { state: "tabs" } satisfies RouteDataProperties, children: [ { path: "", @@ -497,42 +546,42 @@ const routes: Routes = [ component: CurrentTabComponent, canActivate: [authGuard], canMatch: [extensionRefreshRedirect("/tabs/vault")], - data: { state: "tabs_current" }, + data: { state: "tabs_current" } satisfies RouteDataProperties, runGuardsAndResolvers: "always", }, ...extensionRefreshSwap(VaultFilterComponent, VaultV2Component, { path: "vault", canActivate: [authGuard], canDeactivate: [clearVaultStateGuard], - data: { state: "tabs_vault" }, + data: { state: "tabs_vault" } satisfies RouteDataProperties, }), ...generatorSwap(GeneratorComponent, CredentialGeneratorComponent, { path: "generator", canActivate: [authGuard], - data: { state: "tabs_generator" }, + data: { state: "tabs_generator" } satisfies RouteDataProperties, }), ...extensionRefreshSwap(SettingsComponent, SettingsV2Component, { path: "settings", canActivate: [authGuard], - data: { state: "tabs_settings" }, + data: { state: "tabs_settings" } satisfies RouteDataProperties, }), ...extensionRefreshSwap(SendGroupingsComponent, SendV2Component, { path: "send", canActivate: [authGuard], - data: { state: "tabs_send" }, + data: { state: "tabs_send" } satisfies RouteDataProperties, }), ], }), { path: "account-switcher", component: AccountSwitcherComponent, - data: { state: "account-switcher", doNotSaveUrl: true }, + data: { state: "account-switcher", doNotSaveUrl: true } satisfies RouteDataProperties, }, { path: "trash", component: TrashComponent, canActivate: [authGuard], - data: { state: "trash" }, + data: { state: "trash" } satisfies RouteDataProperties, }, ]; diff --git a/apps/browser/src/popup/services/services.module.ts b/apps/browser/src/popup/services/services.module.ts index 129744fd3bc..024b4f46315 100644 --- a/apps/browser/src/popup/services/services.module.ts +++ b/apps/browser/src/popup/services/services.module.ts @@ -16,7 +16,7 @@ import { CLIENT_TYPE, } from "@bitwarden/angular/services/injection-tokens"; import { JslibServicesModule } from "@bitwarden/angular/services/jslib-services.module"; -import { AnonLayoutWrapperDataService } from "@bitwarden/auth/angular"; +import { AnonLayoutWrapperDataService, LockComponentService } from "@bitwarden/auth/angular"; import { LockService, PinServiceAbstraction } from "@bitwarden/auth/common"; import { EventCollectionService as EventCollectionServiceAbstraction } from "@bitwarden/common/abstractions/event/event-collection.service"; import { NotificationsService } from "@bitwarden/common/abstractions/notifications.service"; @@ -62,8 +62,6 @@ import { AbstractStorageService, ObservableStorageService, } from "@bitwarden/common/platform/abstractions/storage.service"; -import { BiometricStateService } from "@bitwarden/common/platform/biometrics/biometric-state.service"; -import { BiometricsService } from "@bitwarden/common/platform/biometrics/biometric.service"; import { Message, MessageListener, MessageSender } from "@bitwarden/common/platform/messaging"; // eslint-disable-next-line no-restricted-imports -- Used for dependency injection import { SubjectMessageSender } from "@bitwarden/common/platform/messaging/internal"; @@ -89,6 +87,7 @@ import { FolderService as FolderServiceAbstraction } from "@bitwarden/common/vau import { TotpService as TotpServiceAbstraction } from "@bitwarden/common/vault/abstractions/totp.service"; import { TotpService } from "@bitwarden/common/vault/services/totp.service"; import { DialogService, ToastService } from "@bitwarden/components"; +import { BiometricsService, BiometricStateService } from "@bitwarden/key-management"; import { PasswordRepromptService } from "@bitwarden/vault"; import { ForegroundLockService } from "../../auth/popup/accounts/foreground-lock.service"; @@ -96,6 +95,7 @@ import { ExtensionAnonLayoutWrapperDataService } from "../../auth/popup/extensio import { AutofillService as AutofillServiceAbstraction } from "../../autofill/services/abstractions/autofill.service"; import AutofillService from "../../autofill/services/autofill.service"; import MainBackground from "../../background/main.background"; +import { ForegroundBrowserBiometricsService } from "../../key-management/biometrics/foreground-browser-biometrics"; import { BrowserApi } from "../../platform/browser/browser-api"; import { runInsideAngular } from "../../platform/browser/run-inside-angular.operator"; /* eslint-disable no-restricted-imports */ @@ -111,13 +111,13 @@ import { BrowserCryptoService } from "../../platform/services/browser-crypto.ser import { BrowserEnvironmentService } from "../../platform/services/browser-environment.service"; import BrowserLocalStorageService from "../../platform/services/browser-local-storage.service"; import { BrowserScriptInjectorService } from "../../platform/services/browser-script-injector.service"; -import { ForegroundBrowserBiometricsService } from "../../platform/services/foreground-browser-biometrics"; import I18nService from "../../platform/services/i18n.service"; import { ForegroundPlatformUtilsService } from "../../platform/services/platform-utils/foreground-platform-utils.service"; import { ForegroundTaskSchedulerService } from "../../platform/services/task-scheduler/foreground-task-scheduler.service"; import { BrowserStorageServiceProvider } from "../../platform/storage/browser-storage-service.provider"; import { ForegroundMemoryStorageService } from "../../platform/storage/foreground-memory-storage.service"; import { fromChromeRuntimeMessaging } from "../../platform/utils/from-chrome-runtime-messaging"; +import { ExtensionLockComponentService } from "../../services/extension-lock-component.service"; import { ForegroundVaultTimeoutService } from "../../services/vault-timeout/foreground-vault-timeout.service"; import { BrowserSendStateService } from "../../tools/popup/services/browser-send-state.service"; import { FilePopoutUtilsService } from "../../tools/popup/services/file-popout-utils.service"; @@ -537,6 +537,11 @@ const safeProviders: SafeProvider[] = [ provide: CLIENT_TYPE, useValue: ClientType.Browser, }), + safeProvider({ + provide: LockComponentService, + useClass: ExtensionLockComponentService, + deps: [], + }), safeProvider({ provide: Fido2UserVerificationService, useClass: Fido2UserVerificationService, diff --git a/apps/browser/src/services/extension-lock-component.service.spec.ts b/apps/browser/src/services/extension-lock-component.service.spec.ts new file mode 100644 index 00000000000..f537897cf8d --- /dev/null +++ b/apps/browser/src/services/extension-lock-component.service.spec.ts @@ -0,0 +1,325 @@ +import { TestBed } from "@angular/core/testing"; +import { mock, MockProxy } from "jest-mock-extended"; +import { firstValueFrom, of } from "rxjs"; + +import { BiometricsDisableReason, UnlockOptions } from "@bitwarden/auth/angular"; +import { + PinServiceAbstraction, + UserDecryptionOptionsServiceAbstraction, +} from "@bitwarden/auth/common"; +import { VaultTimeoutSettingsService } from "@bitwarden/common/abstractions/vault-timeout/vault-timeout-settings.service"; +import { CryptoService } from "@bitwarden/common/platform/abstractions/crypto.service"; +import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; +import { UserId } from "@bitwarden/common/types/guid"; +import { BiometricsService } from "@bitwarden/key-management"; + +import { BrowserRouterService } from "../platform/popup/services/browser-router.service"; + +import { ExtensionLockComponentService } from "./extension-lock-component.service"; + +describe("ExtensionLockComponentService", () => { + let service: ExtensionLockComponentService; + + let userDecryptionOptionsService: MockProxy; + let platformUtilsService: MockProxy; + let biometricsService: MockProxy; + let pinService: MockProxy; + let vaultTimeoutSettingsService: MockProxy; + let cryptoService: MockProxy; + let routerService: MockProxy; + + beforeEach(() => { + userDecryptionOptionsService = mock(); + platformUtilsService = mock(); + biometricsService = mock(); + pinService = mock(); + vaultTimeoutSettingsService = mock(); + cryptoService = mock(); + routerService = mock(); + + TestBed.configureTestingModule({ + providers: [ + ExtensionLockComponentService, + { + provide: UserDecryptionOptionsServiceAbstraction, + useValue: userDecryptionOptionsService, + }, + { + provide: PlatformUtilsService, + useValue: platformUtilsService, + }, + { + provide: BiometricsService, + useValue: biometricsService, + }, + { + provide: PinServiceAbstraction, + useValue: pinService, + }, + { + provide: VaultTimeoutSettingsService, + useValue: vaultTimeoutSettingsService, + }, + { + provide: CryptoService, + useValue: cryptoService, + }, + { + provide: BrowserRouterService, + useValue: routerService, + }, + ], + }); + + service = TestBed.inject(ExtensionLockComponentService); + }); + + it("instantiates", () => { + expect(service).not.toBeFalsy(); + }); + + describe("getPreviousUrl", () => { + it("returns the previous URL", () => { + routerService.getPreviousUrl.mockReturnValue("previousUrl"); + expect(service.getPreviousUrl()).toBe("previousUrl"); + }); + }); + + describe("getBiometricsError", () => { + it("returns a biometric error description when given a valid error type", () => { + expect( + service.getBiometricsError({ + message: "startDesktop", + }), + ).toBe("startDesktopDesc"); + }); + + it("returns null when given an invalid error type", () => { + expect( + service.getBiometricsError({ + message: "invalidError", + }), + ).toBeNull(); + }); + + it("returns null when given a null input", () => { + expect(service.getBiometricsError(null)).toBeNull(); + }); + }); + + describe("isWindowVisible", () => { + it("throws an error", async () => { + await expect(service.isWindowVisible()).rejects.toThrow("Method not implemented."); + }); + }); + + describe("getBiometricsUnlockBtnText", () => { + it("returns the biometric unlock button text", () => { + expect(service.getBiometricsUnlockBtnText()).toBe("unlockWithBiometrics"); + }); + }); + + describe("getAvailableUnlockOptions$", () => { + interface MockInputs { + hasMasterPassword: boolean; + osSupportsBiometric: boolean; + biometricLockSet: boolean; + hasBiometricEncryptedUserKeyStored: boolean; + platformSupportsSecureStorage: boolean; + pinDecryptionAvailable: boolean; + } + + const table: [MockInputs, UnlockOptions][] = [ + [ + // MP + PIN + Biometrics available + { + hasMasterPassword: true, + osSupportsBiometric: true, + biometricLockSet: true, + hasBiometricEncryptedUserKeyStored: true, + platformSupportsSecureStorage: true, + pinDecryptionAvailable: true, + }, + { + masterPassword: { + enabled: true, + }, + pin: { + enabled: true, + }, + biometrics: { + enabled: true, + disableReason: null, + }, + }, + ], + [ + // PIN + Biometrics available + { + hasMasterPassword: false, + osSupportsBiometric: true, + biometricLockSet: true, + hasBiometricEncryptedUserKeyStored: true, + platformSupportsSecureStorage: true, + pinDecryptionAvailable: true, + }, + { + masterPassword: { + enabled: false, + }, + pin: { + enabled: true, + }, + biometrics: { + enabled: true, + disableReason: null, + }, + }, + ], + [ + // Biometrics available: user key stored with no secure storage + { + hasMasterPassword: false, + osSupportsBiometric: true, + biometricLockSet: true, + hasBiometricEncryptedUserKeyStored: true, + platformSupportsSecureStorage: false, + pinDecryptionAvailable: false, + }, + { + masterPassword: { + enabled: false, + }, + pin: { + enabled: false, + }, + biometrics: { + enabled: true, + disableReason: null, + }, + }, + ], + [ + // Biometrics available: no user key stored with no secure storage + { + hasMasterPassword: false, + osSupportsBiometric: true, + biometricLockSet: true, + hasBiometricEncryptedUserKeyStored: false, + platformSupportsSecureStorage: false, + pinDecryptionAvailable: false, + }, + { + masterPassword: { + enabled: false, + }, + pin: { + enabled: false, + }, + biometrics: { + enabled: true, + disableReason: null, + }, + }, + ], + [ + // Biometrics not available: biometric lock not set + { + hasMasterPassword: false, + osSupportsBiometric: true, + biometricLockSet: false, + hasBiometricEncryptedUserKeyStored: true, + platformSupportsSecureStorage: true, + pinDecryptionAvailable: false, + }, + { + masterPassword: { + enabled: false, + }, + pin: { + enabled: false, + }, + biometrics: { + enabled: false, + disableReason: BiometricsDisableReason.EncryptedKeysUnavailable, + }, + }, + ], + [ + // Biometrics not available: user key not stored + { + hasMasterPassword: false, + osSupportsBiometric: true, + biometricLockSet: true, + hasBiometricEncryptedUserKeyStored: false, + platformSupportsSecureStorage: true, + pinDecryptionAvailable: false, + }, + { + masterPassword: { + enabled: false, + }, + pin: { + enabled: false, + }, + biometrics: { + enabled: false, + disableReason: BiometricsDisableReason.EncryptedKeysUnavailable, + }, + }, + ], + [ + // Biometrics not available: OS doesn't support + { + hasMasterPassword: false, + osSupportsBiometric: false, + biometricLockSet: true, + hasBiometricEncryptedUserKeyStored: true, + platformSupportsSecureStorage: true, + pinDecryptionAvailable: false, + }, + { + masterPassword: { + enabled: false, + }, + pin: { + enabled: false, + }, + biometrics: { + enabled: false, + disableReason: BiometricsDisableReason.NotSupportedOnOperatingSystem, + }, + }, + ], + ]; + + test.each(table)("returns unlock options", async (mockInputs, expectedOutput) => { + const userId = "userId" as UserId; + const userDecryptionOptions = { + hasMasterPassword: mockInputs.hasMasterPassword, + }; + + // MP + userDecryptionOptionsService.userDecryptionOptionsById$.mockReturnValue( + of(userDecryptionOptions), + ); + + // Biometrics + biometricsService.supportsBiometric.mockResolvedValue(mockInputs.osSupportsBiometric); + vaultTimeoutSettingsService.isBiometricLockSet.mockResolvedValue(mockInputs.biometricLockSet); + cryptoService.hasUserKeyStored.mockResolvedValue( + mockInputs.hasBiometricEncryptedUserKeyStored, + ); + platformUtilsService.supportsSecureStorage.mockReturnValue( + mockInputs.platformSupportsSecureStorage, + ); + + // PIN + pinService.isPinDecryptionAvailable.mockResolvedValue(mockInputs.pinDecryptionAvailable); + + const unlockOptions = await firstValueFrom(service.getAvailableUnlockOptions$(userId)); + + expect(unlockOptions).toEqual(expectedOutput); + }); + }); +}); diff --git a/apps/browser/src/services/extension-lock-component.service.ts b/apps/browser/src/services/extension-lock-component.service.ts new file mode 100644 index 00000000000..58514fa2b17 --- /dev/null +++ b/apps/browser/src/services/extension-lock-component.service.ts @@ -0,0 +1,117 @@ +import { inject } from "@angular/core"; +import { combineLatest, defer, map, Observable } from "rxjs"; + +import { + BiometricsDisableReason, + LockComponentService, + UnlockOptions, +} from "@bitwarden/auth/angular"; +import { + PinServiceAbstraction, + UserDecryptionOptionsServiceAbstraction, +} from "@bitwarden/auth/common"; +import { VaultTimeoutSettingsService } from "@bitwarden/common/abstractions/vault-timeout/vault-timeout-settings.service"; +import { CryptoService } from "@bitwarden/common/platform/abstractions/crypto.service"; +import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; +import { KeySuffixOptions } from "@bitwarden/common/platform/enums"; +import { UserId } from "@bitwarden/common/types/guid"; +import { BiometricsService } from "@bitwarden/key-management"; + +import { BiometricErrors, BiometricErrorTypes } from "../models/biometricErrors"; +import { BrowserRouterService } from "../platform/popup/services/browser-router.service"; + +export class ExtensionLockComponentService implements LockComponentService { + private readonly userDecryptionOptionsService = inject(UserDecryptionOptionsServiceAbstraction); + private readonly platformUtilsService = inject(PlatformUtilsService); + private readonly biometricsService = inject(BiometricsService); + private readonly pinService = inject(PinServiceAbstraction); + private readonly vaultTimeoutSettingsService = inject(VaultTimeoutSettingsService); + private readonly cryptoService = inject(CryptoService); + private readonly routerService = inject(BrowserRouterService); + + getPreviousUrl(): string | null { + return this.routerService.getPreviousUrl(); + } + + getBiometricsError(error: any): string | null { + const biometricsError = BiometricErrors[error?.message as BiometricErrorTypes]; + + if (!biometricsError) { + return null; + } + + return biometricsError.description; + } + + async isWindowVisible(): Promise { + throw new Error("Method not implemented."); + } + + getBiometricsUnlockBtnText(): string { + return "unlockWithBiometrics"; + } + + private async isBiometricLockSet(userId: UserId): Promise { + const biometricLockSet = await this.vaultTimeoutSettingsService.isBiometricLockSet(userId); + const hasBiometricEncryptedUserKeyStored = await this.cryptoService.hasUserKeyStored( + KeySuffixOptions.Biometric, + userId, + ); + const platformSupportsSecureStorage = this.platformUtilsService.supportsSecureStorage(); + + return ( + biometricLockSet && (hasBiometricEncryptedUserKeyStored || !platformSupportsSecureStorage) + ); + } + + private getBiometricsDisabledReason( + osSupportsBiometric: boolean, + biometricLockSet: boolean, + ): BiometricsDisableReason | null { + if (!osSupportsBiometric) { + return BiometricsDisableReason.NotSupportedOnOperatingSystem; + } else if (!biometricLockSet) { + return BiometricsDisableReason.EncryptedKeysUnavailable; + } + + return null; + } + + getAvailableUnlockOptions$(userId: UserId): Observable { + return combineLatest([ + // Note: defer is preferable b/c it delays the execution of the function until the observable is subscribed to + defer(() => this.biometricsService.supportsBiometric()), + defer(() => this.isBiometricLockSet(userId)), + this.userDecryptionOptionsService.userDecryptionOptionsById$(userId), + defer(() => this.pinService.isPinDecryptionAvailable(userId)), + ]).pipe( + map( + ([ + supportsBiometric, + isBiometricsLockSet, + userDecryptionOptions, + pinDecryptionAvailable, + ]) => { + const disableReason = this.getBiometricsDisabledReason( + supportsBiometric, + isBiometricsLockSet, + ); + + const unlockOpts: UnlockOptions = { + masterPassword: { + enabled: userDecryptionOptions.hasMasterPassword, + }, + pin: { + enabled: pinDecryptionAvailable, + }, + biometrics: { + enabled: supportsBiometric && isBiometricsLockSet, + disableReason: disableReason, + }, + }; + return unlockOpts; + }, + ), + ); + } +} diff --git a/apps/browser/src/tools/popup/send-v2/add-edit/send-add-edit.component.html b/apps/browser/src/tools/popup/send-v2/add-edit/send-add-edit.component.html index b3783bfed3a..40c942539f6 100644 --- a/apps/browser/src/tools/popup/send-v2/add-edit/send-add-edit.component.html +++ b/apps/browser/src/tools/popup/send-v2/add-edit/send-add-edit.component.html @@ -4,7 +4,8 @@ diff --git a/apps/browser/src/tools/popup/send-v2/add-edit/send-add-edit.component.ts b/apps/browser/src/tools/popup/send-v2/add-edit/send-add-edit.component.ts index c84b9717df1..20b472f97f3 100644 --- a/apps/browser/src/tools/popup/send-v2/add-edit/send-add-edit.component.ts +++ b/apps/browser/src/tools/popup/send-v2/add-edit/send-add-edit.component.ts @@ -2,12 +2,13 @@ import { CommonModule, Location } from "@angular/common"; import { Component } from "@angular/core"; import { takeUntilDestroyed } from "@angular/core/rxjs-interop"; import { FormsModule } from "@angular/forms"; -import { ActivatedRoute, Params } from "@angular/router"; +import { ActivatedRoute, Params, Router } from "@angular/router"; import { map, switchMap } from "rxjs"; import { JslibModule } from "@bitwarden/angular/jslib.module"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { SendType } from "@bitwarden/common/tools/send/enums/send-type"; +import { SendView } from "@bitwarden/common/tools/send/models/view/send.view"; import { SendApiService } from "@bitwarden/common/tools/send/services/send-api.service.abstraction"; import { SendId } from "@bitwarden/common/types/guid"; import { @@ -95,14 +96,25 @@ export class SendAddEditComponent { private sendApiService: SendApiService, private toastService: ToastService, private dialogService: DialogService, + private router: Router, ) { this.subscribeToParams(); } /** - * Handles the event when the send is saved. + * Handles the event when the send is created. */ - onSendSaved() { + async onSendCreated(send: SendView) { + await this.router.navigate(["/send-created"], { + queryParams: { sendId: send.id }, + }); + return; + } + + /** + * Handles the event when the send is updated. + */ + onSendUpdated(send: SendView) { this.location.back(); } diff --git a/apps/browser/src/tools/popup/send-v2/send-created/send-created.component.html b/apps/browser/src/tools/popup/send-v2/send-created/send-created.component.html index 9b56fa74d91..c97d3da1396 100644 --- a/apps/browser/src/tools/popup/send-v2/send-created/send-created.component.html +++ b/apps/browser/src/tools/popup/send-v2/send-created/send-created.component.html @@ -1,6 +1,11 @@
- + diff --git a/apps/browser/src/tools/popup/send-v2/send-created/send-created.component.spec.ts b/apps/browser/src/tools/popup/send-v2/send-created/send-created.component.spec.ts index 413f22565e1..bcc4d2e2ccb 100644 --- a/apps/browser/src/tools/popup/send-v2/send-created/send-created.component.spec.ts +++ b/apps/browser/src/tools/popup/send-v2/send-created/send-created.component.spec.ts @@ -1,6 +1,6 @@ import { CommonModule, Location } from "@angular/common"; import { ComponentFixture, TestBed } from "@angular/core/testing"; -import { ActivatedRoute, RouterLink } from "@angular/router"; +import { ActivatedRoute, Router, RouterLink } from "@angular/router"; import { RouterTestingModule } from "@angular/router/testing"; import { MockProxy, mock } from "jest-mock-extended"; import { of } from "rxjs"; @@ -33,6 +33,7 @@ describe("SendCreatedComponent", () => { let location: MockProxy; let activatedRoute: MockProxy; let environmentService: MockProxy; + let router: MockProxy; const sendId = "test-send-id"; const deletionDate = new Date(); @@ -52,6 +53,7 @@ describe("SendCreatedComponent", () => { location = mock(); activatedRoute = mock(); environmentService = mock(); + router = mock(); Object.defineProperty(environmentService, "environment$", { configurable: true, get: () => of(new SelfHostedEnvironment({ webVault: "https://example.com" })), @@ -89,6 +91,7 @@ describe("SendCreatedComponent", () => { { provide: ConfigService, useValue: mock() }, { provide: EnvironmentService, useValue: environmentService }, { provide: PopupRouterCacheService, useValue: mock() }, + { provide: Router, useValue: router }, ], }).compileComponents(); }); @@ -109,10 +112,10 @@ describe("SendCreatedComponent", () => { expect(component["daysAvailable"]).toBe(7); }); - it("should navigate back on close", () => { + it("should navigate back to send list on close", async () => { fixture.detectChanges(); - component.close(); - expect(location.back).toHaveBeenCalled(); + await component.close(); + expect(router.navigate).toHaveBeenCalledWith(["/tabs/send"]); }); describe("getDaysAvailable", () => { diff --git a/apps/browser/src/tools/popup/send-v2/send-created/send-created.component.ts b/apps/browser/src/tools/popup/send-v2/send-created/send-created.component.ts index 92339774d05..4ed4da2f81d 100644 --- a/apps/browser/src/tools/popup/send-v2/send-created/send-created.component.ts +++ b/apps/browser/src/tools/popup/send-v2/send-created/send-created.component.ts @@ -1,7 +1,7 @@ -import { CommonModule, Location } from "@angular/common"; +import { CommonModule } from "@angular/common"; import { Component } from "@angular/core"; import { takeUntilDestroyed } from "@angular/core/rxjs-interop"; -import { ActivatedRoute, RouterLink } from "@angular/router"; +import { ActivatedRoute, Router, RouterLink, RouterModule } from "@angular/router"; import { firstValueFrom } from "rxjs"; import { JslibModule } from "@bitwarden/angular/jslib.module"; @@ -30,6 +30,7 @@ import { PopupPageComponent } from "../../../../platform/popup/layout/popup-page PopupHeaderComponent, PopupPageComponent, RouterLink, + RouterModule, PopupFooterComponent, IconModule, ], @@ -45,10 +46,11 @@ export class SendCreatedComponent { private sendService: SendService, private route: ActivatedRoute, private toastService: ToastService, - private location: Location, + private router: Router, private environmentService: EnvironmentService, ) { const sendId = this.route.snapshot.queryParamMap.get("sendId"); + this.sendService.sendViews$.pipe(takeUntilDestroyed()).subscribe((sendViews) => { this.send = sendViews.find((s) => s.id === sendId); if (this.send) { @@ -62,8 +64,8 @@ export class SendCreatedComponent { return Math.max(0, Math.ceil((send.deletionDate.getTime() - now) / (1000 * 60 * 60 * 24))); } - close() { - this.location.back(); + async close() { + await this.router.navigate(["/tabs/send"]); } async copyLink() { diff --git a/apps/browser/src/tools/popup/send-v2/send-file-popout-dialog/send-file-popout-dialog-container.component.ts b/apps/browser/src/tools/popup/send-v2/send-file-popout-dialog/send-file-popout-dialog-container.component.ts index 33f121431fc..d3d481063e8 100644 --- a/apps/browser/src/tools/popup/send-v2/send-file-popout-dialog/send-file-popout-dialog-container.component.ts +++ b/apps/browser/src/tools/popup/send-v2/send-file-popout-dialog/send-file-popout-dialog-container.component.ts @@ -2,6 +2,7 @@ import { CommonModule } from "@angular/common"; import { Component, Input, OnInit } from "@angular/core"; import { JslibModule } from "@bitwarden/angular/jslib.module"; +import { SendType } from "@bitwarden/common/tools/send/enums/send-type"; import { DialogService } from "@bitwarden/components"; import { SendFormConfig } from "@bitwarden/send-ui"; @@ -24,7 +25,11 @@ export class SendFilePopoutDialogContainerComponent implements OnInit { ) {} ngOnInit() { - if (this.config.mode === "add" && this.filePopoutUtilsService.showFilePopoutMessage(window)) { + if ( + this.config.sendType === SendType.File && + this.config.mode === "add" && + this.filePopoutUtilsService.showFilePopoutMessage(window) + ) { this.dialogService.open(SendFilePopoutDialogComponent); } } diff --git a/apps/browser/src/tools/popup/send-v2/send-v2.component.html b/apps/browser/src/tools/popup/send-v2/send-v2.component.html index 23582f19115..23cc692a598 100644 --- a/apps/browser/src/tools/popup/send-v2/send-v2.component.html +++ b/apps/browser/src/tools/popup/send-v2/send-v2.component.html @@ -10,7 +10,7 @@ {{ "sendDisabledWarning" | i18n }} - + @@ -23,7 +23,11 @@ {{ "sendsNoItemsTitle" | i18n }} {{ "sendsNoItemsMessage" | i18n }} - +
diff --git a/apps/browser/src/vault/popup/components/vault-v2/attachments/open-attachments/open-attachments.component.html b/apps/browser/src/vault/popup/components/vault-v2/attachments/open-attachments/open-attachments.component.html index 6b2d8eaa033..6345c3ea4e1 100644 --- a/apps/browser/src/vault/popup/components/vault-v2/attachments/open-attachments/open-attachments.component.html +++ b/apps/browser/src/vault/popup/components/vault-v2/attachments/open-attachments/open-attachments.component.html @@ -1,11 +1,9 @@ diff --git a/apps/web/src/app/billing/organizations/organization-subscription-selfhost.component.html b/apps/web/src/app/billing/organizations/organization-subscription-selfhost.component.html index 0a029de79dc..5a1ccc0768a 100644 --- a/apps/web/src/app/billing/organizations/organization-subscription-selfhost.component.html +++ b/apps/web/src/app/billing/organizations/organization-subscription-selfhost.component.html @@ -90,7 +90,7 @@ - {{ "billingSyncDesc" | i18n }} + {{ "automaticBillingSyncDesc" | i18n }} @@ -100,7 +100,7 @@ type="button" (click)="manageBillingSyncSelfHosted()" > - {{ "manageBillingSync" | i18n }} + {{ "manageBillingTokenSync" | i18n }} @@ -119,26 +114,6 @@ {{ "clone" | i18n }} - - - - - - diff --git a/apps/web/src/app/vault/individual-vault/bulk-action-dialogs/bulk-move-dialog/bulk-move-dialog.component.html b/apps/web/src/app/vault/individual-vault/bulk-action-dialogs/bulk-move-dialog/bulk-move-dialog.component.html index 59341a712d5..6291a7641dd 100644 --- a/apps/web/src/app/vault/individual-vault/bulk-action-dialogs/bulk-move-dialog/bulk-move-dialog.component.html +++ b/apps/web/src/app/vault/individual-vault/bulk-action-dialogs/bulk-move-dialog/bulk-move-dialog.component.html @@ -1,7 +1,7 @@ - {{ ((vaultBulkManagementActionEnabled$ | async) ? "addToFolder" : "moveSelected") | i18n }} + {{ "addToFolder" | i18n }}

{{ "moveSelectedItemsDesc" | i18n: cipherIds.length }}

diff --git a/apps/web/src/app/vault/individual-vault/bulk-action-dialogs/bulk-move-dialog/bulk-move-dialog.component.ts b/apps/web/src/app/vault/individual-vault/bulk-action-dialogs/bulk-move-dialog/bulk-move-dialog.component.ts index 252cdc7ac54..cdf45d0669c 100644 --- a/apps/web/src/app/vault/individual-vault/bulk-action-dialogs/bulk-move-dialog/bulk-move-dialog.component.ts +++ b/apps/web/src/app/vault/individual-vault/bulk-action-dialogs/bulk-move-dialog/bulk-move-dialog.component.ts @@ -3,8 +3,6 @@ import { Component, Inject, OnInit } from "@angular/core"; import { FormBuilder, Validators } from "@angular/forms"; import { firstValueFrom, Observable } from "rxjs"; -import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum"; -import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service"; @@ -47,10 +45,6 @@ export class BulkMoveDialogComponent implements OnInit { }); folders$: Observable; - protected vaultBulkManagementActionEnabled$ = this.configService.getFeatureFlag$( - FeatureFlag.VaultBulkManagementAction, - ); - constructor( @Inject(DIALOG_DATA) params: BulkMoveDialogParams, private dialogRef: DialogRef, @@ -59,7 +53,6 @@ export class BulkMoveDialogComponent implements OnInit { private i18nService: I18nService, private folderService: FolderService, private formBuilder: FormBuilder, - private configService: ConfigService, ) { this.cipherIds = params.cipherIds ?? []; } diff --git a/apps/web/src/app/vault/individual-vault/organization-badge/organization-name-badge.component.ts b/apps/web/src/app/vault/individual-vault/organization-badge/organization-name-badge.component.ts index 6d53b8ad720..3e37d4998de 100644 --- a/apps/web/src/app/vault/individual-vault/organization-badge/organization-name-badge.component.ts +++ b/apps/web/src/app/vault/individual-vault/organization-badge/organization-name-badge.component.ts @@ -1,13 +1,12 @@ import { Component, Input, OnChanges } from "@angular/core"; import { firstValueFrom } from "rxjs"; +import { Unassigned } from "@bitwarden/admin-console/common"; import { AvatarService } from "@bitwarden/common/auth/abstractions/avatar.service"; import { TokenService } from "@bitwarden/common/auth/abstractions/token.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { Utils } from "@bitwarden/common/platform/misc/utils"; -import { Unassigned } from "../vault-filter/shared/models/routed-vault-filter.model"; - @Component({ selector: "app-org-badge", templateUrl: "organization-name-badge.component.html", diff --git a/apps/web/src/app/vault/individual-vault/vault-filter/services/abstractions/vault-filter.service.ts b/apps/web/src/app/vault/individual-vault/vault-filter/services/abstractions/vault-filter.service.ts index 836cba22016..b18ee76e9c8 100644 --- a/apps/web/src/app/vault/individual-vault/vault-filter/services/abstractions/vault-filter.service.ts +++ b/apps/web/src/app/vault/individual-vault/vault-filter/services/abstractions/vault-filter.service.ts @@ -1,11 +1,11 @@ import { Observable } from "rxjs"; +import { CollectionAdminView } from "@bitwarden/admin-console/common"; import { Organization } from "@bitwarden/common/admin-console/models/domain/organization"; import { CollectionView } from "@bitwarden/common/src/vault/models/view/collection.view"; import { FolderView } from "@bitwarden/common/src/vault/models/view/folder.view"; import { TreeNode } from "@bitwarden/common/vault/models/domain/tree-node"; -import { CollectionAdminView } from "../../../../core/views/collection-admin.view"; import { CipherTypeFilter, CollectionFilter, diff --git a/apps/web/src/app/vault/individual-vault/vault-filter/services/routed-vault-filter-bridge.service.ts b/apps/web/src/app/vault/individual-vault/vault-filter/services/routed-vault-filter-bridge.service.ts index 08ce9b67ba5..1f0a9e135b5 100644 --- a/apps/web/src/app/vault/individual-vault/vault-filter/services/routed-vault-filter-bridge.service.ts +++ b/apps/web/src/app/vault/individual-vault/vault-filter/services/routed-vault-filter-bridge.service.ts @@ -2,15 +2,12 @@ import { Injectable } from "@angular/core"; import { Router } from "@angular/router"; import { combineLatest, map, Observable } from "rxjs"; +import { Unassigned } from "@bitwarden/admin-console/common"; import { TreeNode } from "@bitwarden/common/vault/models/domain/tree-node"; import { ServiceUtils } from "@bitwarden/common/vault/service-utils"; import { RoutedVaultFilterBridge } from "../shared/models/routed-vault-filter-bridge.model"; -import { - RoutedVaultFilterModel, - Unassigned, - All, -} from "../shared/models/routed-vault-filter.model"; +import { RoutedVaultFilterModel, All } from "../shared/models/routed-vault-filter.model"; import { VaultFilter } from "../shared/models/vault-filter.model"; import { CipherTypeFilter, diff --git a/apps/web/src/app/vault/individual-vault/vault-filter/services/vault-filter.service.ts b/apps/web/src/app/vault/individual-vault/vault-filter/services/vault-filter.service.ts index ac20f86d0ee..d8abfb2f794 100644 --- a/apps/web/src/app/vault/individual-vault/vault-filter/services/vault-filter.service.ts +++ b/apps/web/src/app/vault/individual-vault/vault-filter/services/vault-filter.service.ts @@ -10,6 +10,7 @@ import { switchMap, } from "rxjs"; +import { CollectionAdminView } from "@bitwarden/admin-console/common"; import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction"; import { PolicyType } from "@bitwarden/common/admin-console/enums"; @@ -26,7 +27,6 @@ import { FolderView } from "@bitwarden/common/vault/models/view/folder.view"; import { ServiceUtils } from "@bitwarden/common/vault/service-utils"; import { COLLAPSED_GROUPINGS } from "@bitwarden/common/vault/services/key-state/collapsed-groupings.state"; -import { CollectionAdminView } from "../../../core/views/collection-admin.view"; import { CipherTypeFilter, CollectionFilter, diff --git a/apps/web/src/app/vault/individual-vault/vault-filter/shared/models/filter-function.spec.ts b/apps/web/src/app/vault/individual-vault/vault-filter/shared/models/filter-function.spec.ts index 786b2b1c7aa..397b7810606 100644 --- a/apps/web/src/app/vault/individual-vault/vault-filter/shared/models/filter-function.spec.ts +++ b/apps/web/src/app/vault/individual-vault/vault-filter/shared/models/filter-function.spec.ts @@ -1,8 +1,9 @@ +import { Unassigned } from "@bitwarden/admin-console/common"; import { CipherType } from "@bitwarden/common/vault/enums"; import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view"; import { createFilterFunction } from "./filter-function"; -import { Unassigned, All } from "./routed-vault-filter.model"; +import { All } from "./routed-vault-filter.model"; describe("createFilter", () => { describe("given a generic cipher", () => { diff --git a/apps/web/src/app/vault/individual-vault/vault-filter/shared/models/filter-function.ts b/apps/web/src/app/vault/individual-vault/vault-filter/shared/models/filter-function.ts index 4716eb631b1..4b038512581 100644 --- a/apps/web/src/app/vault/individual-vault/vault-filter/shared/models/filter-function.ts +++ b/apps/web/src/app/vault/individual-vault/vault-filter/shared/models/filter-function.ts @@ -1,7 +1,8 @@ +import { Unassigned } from "@bitwarden/admin-console/common"; import { CipherType } from "@bitwarden/common/vault/enums"; import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view"; -import { All, RoutedVaultFilterModel, Unassigned } from "./routed-vault-filter.model"; +import { All, RoutedVaultFilterModel } from "./routed-vault-filter.model"; export type FilterFunction = (cipher: CipherView) => boolean; diff --git a/apps/web/src/app/vault/individual-vault/vault-filter/shared/models/routed-vault-filter-bridge.model.ts b/apps/web/src/app/vault/individual-vault/vault-filter/shared/models/routed-vault-filter-bridge.model.ts index 2f6047b6bbc..fe236a089e0 100644 --- a/apps/web/src/app/vault/individual-vault/vault-filter/shared/models/routed-vault-filter-bridge.model.ts +++ b/apps/web/src/app/vault/individual-vault/vault-filter/shared/models/routed-vault-filter-bridge.model.ts @@ -1,3 +1,4 @@ +import { Unassigned } from "@bitwarden/admin-console/common"; import { CipherType } from "@bitwarden/common/vault/enums"; import { TreeNode } from "@bitwarden/common/vault/models/domain/tree-node"; @@ -8,7 +9,6 @@ import { isRoutedVaultFilterItemType, RoutedVaultFilterItemType, RoutedVaultFilterModel, - Unassigned, } from "./routed-vault-filter.model"; import { VaultFilter, VaultFilterFunction } from "./vault-filter.model"; import { diff --git a/apps/web/src/app/vault/individual-vault/vault-filter/shared/models/routed-vault-filter.model.ts b/apps/web/src/app/vault/individual-vault/vault-filter/shared/models/routed-vault-filter.model.ts index 5579c62d4e9..4f2659d6101 100644 --- a/apps/web/src/app/vault/individual-vault/vault-filter/shared/models/routed-vault-filter.model.ts +++ b/apps/web/src/app/vault/individual-vault/vault-filter/shared/models/routed-vault-filter.model.ts @@ -1,5 +1,3 @@ -export const Unassigned = "unassigned"; - export const All = "all"; // TODO: Remove `All` when moving to vertical navigation. diff --git a/apps/web/src/app/vault/individual-vault/vault-filter/shared/models/vault-filter.type.ts b/apps/web/src/app/vault/individual-vault/vault-filter/shared/models/vault-filter.type.ts index fd349069aaa..0cd385bd19d 100644 --- a/apps/web/src/app/vault/individual-vault/vault-filter/shared/models/vault-filter.type.ts +++ b/apps/web/src/app/vault/individual-vault/vault-filter/shared/models/vault-filter.type.ts @@ -1,10 +1,9 @@ +import { CollectionAdminView } from "@bitwarden/admin-console/common"; import { Organization } from "@bitwarden/common/admin-console/models/domain/organization"; import { FolderView } from "@bitwarden/common/src/vault/models/view/folder.view"; import { CipherType } from "@bitwarden/common/vault/enums"; import { ITreeNodeObject } from "@bitwarden/common/vault/models/domain/tree-node"; -import { CollectionAdminView } from "../../../../core/views/collection-admin.view"; - export type CipherStatus = "all" | "favorites" | "trash" | CipherType; export type CipherTypeFilter = ITreeNodeObject & { type: CipherStatus; icon: string }; diff --git a/apps/web/src/app/vault/individual-vault/vault-header/vault-header.component.ts b/apps/web/src/app/vault/individual-vault/vault-header/vault-header.component.ts index 44e523abe61..463a03091e0 100644 --- a/apps/web/src/app/vault/individual-vault/vault-header/vault-header.component.ts +++ b/apps/web/src/app/vault/individual-vault/vault-header/vault-header.component.ts @@ -9,6 +9,7 @@ import { } from "@angular/core"; import { firstValueFrom } from "rxjs"; +import { Unassigned } from "@bitwarden/admin-console/common"; import { JslibModule } from "@bitwarden/angular/jslib.module"; import { Organization } from "@bitwarden/common/admin-console/models/domain/organization"; import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum"; @@ -26,7 +27,6 @@ import { PipesModule } from "../pipes/pipes.module"; import { All, RoutedVaultFilterModel, - Unassigned, } from "../vault-filter/shared/models/routed-vault-filter.model"; @Component({ diff --git a/apps/web/src/app/vault/individual-vault/vault.component.html b/apps/web/src/app/vault/individual-vault/vault.component.html index b19a4509c15..b2c4fda57d0 100644 --- a/apps/web/src/app/vault/individual-vault/vault.component.html +++ b/apps/web/src/app/vault/individual-vault/vault.component.html @@ -54,9 +54,8 @@ [showBulkTrashOptions]="filter.type === 'trash'" [useEvents]="false" [showAdminActions]="false" - [showBulkAddToCollections]="vaultBulkManagementActionEnabled$ | async" + [showBulkAddToCollections]="true" (onEvent)="onVaultItemsEvent($event)" - [vaultBulkManagementActionEnabled]="vaultBulkManagementActionEnabled$ | async" >
| undefined; protected canCreateCollections = false; protected currentSearchText$: Observable; - protected vaultBulkManagementActionEnabled$ = this.configService.getFeatureFlag$( - FeatureFlag.VaultBulkManagementAction, - ); private searchText$ = new Subject(); private refresh$ = new BehaviorSubject(null); private destroy$ = new Subject(); @@ -458,9 +446,6 @@ export class VaultComponent implements OnInit, OnDestroy { case "viewAttachments": await this.editCipherAttachments(event.item); break; - case "viewCipherCollections": - await this.editCipherCollections(event.item); - break; case "clone": await this.cloneCipher(event.item); break; @@ -477,13 +462,6 @@ export class VaultComponent implements OnInit, OnDestroy { case "moveToFolder": await this.bulkMove(event.items); break; - case "moveToOrganization": - if (event.items.length === 1) { - await this.shareCipher(event.items[0]); - } else { - await this.bulkShare(event.items); - } - break; case "copyField": await this.copy(event.item, event.field); break; @@ -566,9 +544,6 @@ export class VaultComponent implements OnInit, OnDestroy { } const canEditAttachments = await this.canEditAttachments(cipher); - const vaultBulkManagementActionEnabled = await firstValueFrom( - this.vaultBulkManagementActionEnabled$, - ); let madeAttachmentChanges = false; @@ -594,7 +569,7 @@ export class VaultComponent implements OnInit, OnDestroy { this.attachmentsModalRef, (comp) => { comp.cipherId = cipher.id; - comp.viewOnly = !canEditAttachments && vaultBulkManagementActionEnabled; + comp.viewOnly = !canEditAttachments; comp.onUploadedAttachment .pipe(takeUntil(this.destroy$)) .subscribe(() => (madeAttachmentChanges = true)); @@ -615,41 +590,6 @@ export class VaultComponent implements OnInit, OnDestroy { }); } - async shareCipher(cipher: CipherView) { - if (cipher.organizationId != null) { - // You cannot move ciphers between organizations - this.showMissingPermissionsError(); - return; - } - - if (cipher?.reprompt !== 0 && !(await this.passwordRepromptService.showPasswordPrompt())) { - this.go({ cipherId: null, itemId: null }); - return; - } - const [modal] = await this.modalService.openViewRef( - ShareComponent, - this.shareModalRef, - (comp) => { - comp.cipherId = cipher.id; - comp.onSharedCipher.pipe(takeUntil(this.destroy$)).subscribe(() => { - modal.close(); - this.refresh(); - }); - }, - ); - } - - async editCipherCollections(cipher: CipherView) { - const dialog = openIndividualVaultCollectionsDialog(this.dialogService, { - data: { cipherId: cipher.id }, - }); - const result = await lastValueFrom(dialog.closed); - - if (result === CollectionsDialogResult.Saved) { - this.refresh(); - } - } - async addCipher(cipherType?: CipherType) { if (this.extensionRefreshEnabled) { return this.addCipherV2(cipherType); @@ -1255,34 +1195,6 @@ export class VaultComponent implements OnInit, OnDestroy { } } - async bulkShare(ciphers: CipherView[]) { - if (!(await this.repromptCipher(ciphers))) { - return; - } - - if (ciphers.some((c) => c.organizationId != null)) { - // You cannot move ciphers between organizations - this.showMissingPermissionsError(); - return; - } - - if (ciphers.length === 0) { - this.toastService.showToast({ - variant: "error", - title: null, - message: this.i18nService.t("nothingSelected"), - }); - return; - } - - const dialog = openBulkShareDialog(this.dialogService, { data: { ciphers } }); - - const result = await lastValueFrom(dialog.closed); - if (result === BulkShareDialogResult.Shared) { - this.refresh(); - } - } - protected deleteCipherWithServer(id: string, permanent: boolean) { return permanent ? this.cipherService.deleteWithServer(id) diff --git a/apps/web/src/app/vault/org-vault/bulk-collections-dialog/bulk-collections-dialog.component.ts b/apps/web/src/app/vault/org-vault/bulk-collections-dialog/bulk-collections-dialog.component.ts index 76e90097d19..2839a6ae607 100644 --- a/apps/web/src/app/vault/org-vault/bulk-collections-dialog/bulk-collections-dialog.component.ts +++ b/apps/web/src/app/vault/org-vault/bulk-collections-dialog/bulk-collections-dialog.component.ts @@ -3,7 +3,10 @@ import { Component, Inject, OnDestroy } from "@angular/core"; import { FormBuilder } from "@angular/forms"; import { combineLatest, of, Subject, switchMap, takeUntil } from "rxjs"; -import { OrganizationUserApiService } from "@bitwarden/admin-console/common"; +import { + CollectionAdminService, + OrganizationUserApiService, +} from "@bitwarden/admin-console/common"; import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; import { Organization } from "@bitwarden/common/admin-console/models/domain/organization"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; @@ -23,7 +26,6 @@ import { PermissionMode, } from "../../../admin-console/organizations/shared/components/access-selector"; import { SharedModule } from "../../../shared"; -import { CollectionAdminService } from "../../core/collection-admin.service"; export interface BulkCollectionsDialogParams { organizationId: string; @@ -79,7 +81,7 @@ export class BulkCollectionsDialogComponent implements OnDestroy { combineLatest([ organization$, groups$, - this.organizationUserApiService.getAllUsers(this.params.organizationId), + this.organizationUserApiService.getAllMiniUserDetails(this.params.organizationId), ]) .pipe(takeUntil(this.destroy$)) .subscribe(([organization, groups, users]) => { diff --git a/apps/web/src/app/vault/org-vault/vault-filter/vault-filter.service.ts b/apps/web/src/app/vault/org-vault/vault-filter/vault-filter.service.ts index c6d4ee590b8..f9717f19f1e 100644 --- a/apps/web/src/app/vault/org-vault/vault-filter/vault-filter.service.ts +++ b/apps/web/src/app/vault/org-vault/vault-filter/vault-filter.service.ts @@ -1,6 +1,7 @@ import { Injectable, OnDestroy } from "@angular/core"; import { map, Observable, ReplaySubject, Subject } from "rxjs"; +import { CollectionAdminView } from "@bitwarden/admin-console/common"; import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; @@ -10,7 +11,6 @@ import { CollectionService } from "@bitwarden/common/vault/abstractions/collecti import { FolderService } from "@bitwarden/common/vault/abstractions/folder/folder.service.abstraction"; import { TreeNode } from "@bitwarden/common/vault/models/domain/tree-node"; -import { CollectionAdminView } from "../../../vault/core/views/collection-admin.view"; import { VaultFilterService as BaseVaultFilterService } from "../../individual-vault/vault-filter/services/vault-filter.service"; import { CollectionFilter } from "../../individual-vault/vault-filter/shared/models/vault-filter.type"; diff --git a/apps/web/src/app/vault/org-vault/vault-header/vault-header.component.ts b/apps/web/src/app/vault/org-vault/vault-header/vault-header.component.ts index 429062917ad..a3d564a9a3a 100644 --- a/apps/web/src/app/vault/org-vault/vault-header/vault-header.component.ts +++ b/apps/web/src/app/vault/org-vault/vault-header/vault-header.component.ts @@ -3,6 +3,11 @@ import { Component, EventEmitter, Input, OnInit, Output } from "@angular/core"; import { Router } from "@angular/router"; import { firstValueFrom } from "rxjs"; +import { + CollectionAdminService, + CollectionAdminView, + Unassigned, +} from "@bitwarden/admin-console/common"; import { JslibModule } from "@bitwarden/angular/jslib.module"; import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; import { Organization } from "@bitwarden/common/admin-console/models/domain/organization"; @@ -22,13 +27,10 @@ import { import { HeaderModule } from "../../../layouts/header/header.module"; import { SharedModule } from "../../../shared"; -import { CollectionAdminView } from "../../../vault/core/views/collection-admin.view"; import { CollectionDialogTabType } from "../../components/collection-dialog"; -import { CollectionAdminService } from "../../core/collection-admin.service"; import { All, RoutedVaultFilterModel, - Unassigned, } from "../../individual-vault/vault-filter/shared/models/routed-vault-filter.model"; @Component({ diff --git a/apps/web/src/app/vault/org-vault/vault.component.ts b/apps/web/src/app/vault/org-vault/vault.component.ts index 2976bfc8c2f..1eea053d1f0 100644 --- a/apps/web/src/app/vault/org-vault/vault.component.ts +++ b/apps/web/src/app/vault/org-vault/vault.component.ts @@ -31,8 +31,9 @@ import { } from "rxjs/operators"; import { - OrganizationUserApiService, - OrganizationUserUserDetailsResponse, + CollectionAdminService, + CollectionAdminView, + Unassigned, } from "@bitwarden/admin-console/common"; import { SearchPipe } from "@bitwarden/angular/pipes/search.pipe"; import { ModalService } from "@bitwarden/angular/services/modal.service"; @@ -76,8 +77,6 @@ import { } from "../components/collection-dialog"; import { VaultItemEvent } from "../components/vault-items/vault-item-event"; import { VaultItemsModule } from "../components/vault-items/vault-items.module"; -import { CollectionAdminService } from "../core/collection-admin.service"; -import { CollectionAdminView } from "../core/views/collection-admin.view"; import { BulkDeleteDialogResult, openBulkDeleteDialog, @@ -89,7 +88,6 @@ import { createFilterFunction } from "../individual-vault/vault-filter/shared/mo import { All, RoutedVaultFilterModel, - Unassigned, } from "../individual-vault/vault-filter/shared/models/routed-vault-filter.model"; import { openViewCipherDialog, @@ -168,8 +166,6 @@ export class VaultComponent implements OnInit, OnDestroy { protected editableCollections$: Observable; protected allCollectionsWithoutUnassigned$: Observable; - protected orgRevokedUsers: OrganizationUserUserDetailsResponse[]; - protected get hideVaultFilters(): boolean { return this.organization?.isProviderUser && !this.organization?.isMember; } @@ -206,7 +202,6 @@ export class VaultComponent implements OnInit, OnDestroy { private totpService: TotpService, private apiService: ApiService, private collectionService: CollectionService, - private organizationUserApiService: OrganizationUserApiService, private toastService: ToastService, private accountService: AccountService, ) {} @@ -358,13 +353,6 @@ export class VaultComponent implements OnInit, OnDestroy { shareReplay({ refCount: true, bufferSize: 1 }), ); - // This will be passed into the usersCanManage call - this.orgRevokedUsers = ( - await this.organizationUserApiService.getAllUsers(await firstValueFrom(organizationId$)) - ).data.filter((user: OrganizationUserUserDetailsResponse) => { - return user.status === -1; - }); - const collections$ = combineLatest([ nestedCollections$, filter$, @@ -610,9 +598,6 @@ export class VaultComponent implements OnInit, OnDestroy { case "viewAttachments": await this.editCipherAttachments(event.item); break; - case "viewCipherCollections": - await this.editCipherCollections(event.item); - break; case "clone": await this.cloneCipher(event.item); break; diff --git a/apps/web/src/app/vault/utils/collection-utils.ts b/apps/web/src/app/vault/utils/collection-utils.ts index b035c40f9f5..2f93e46bed2 100644 --- a/apps/web/src/app/vault/utils/collection-utils.ts +++ b/apps/web/src/app/vault/utils/collection-utils.ts @@ -1,3 +1,4 @@ +import { CollectionAdminView } from "@bitwarden/admin-console/common"; import { TreeNode } from "@bitwarden/common/vault/models/domain/tree-node"; import { CollectionView, @@ -5,8 +6,6 @@ import { } from "@bitwarden/common/vault/models/view/collection.view"; import { ServiceUtils } from "@bitwarden/common/vault/service-utils"; -import { CollectionAdminView } from "../../vault/core/views/collection-admin.view"; - export function getNestedCollectionTree( collections: CollectionAdminView[], ): TreeNode[]; diff --git a/apps/web/src/connectors/duo-redirect.ts b/apps/web/src/connectors/duo-redirect.ts index ddbf75e53ca..2b8a3de4de1 100644 --- a/apps/web/src/connectors/duo-redirect.ts +++ b/apps/web/src/connectors/duo-redirect.ts @@ -54,8 +54,10 @@ function redirectToDuoFrameless(redirectUrl: string) { if ( validateUrl.protocol !== "https:" || - !validateUrl.hostname.endsWith("duosecurity.com") || - !validateUrl.hostname.endsWith("duofederal.com") + !( + validateUrl.hostname.endsWith("duosecurity.com") || + validateUrl.hostname.endsWith("duofederal.com") + ) ) { throw new Error("Invalid redirect URL"); } diff --git a/apps/web/src/locales/en/messages.json b/apps/web/src/locales/en/messages.json index 63dbd6ff9ce..c2bda63c277 100644 --- a/apps/web/src/locales/en/messages.json +++ b/apps/web/src/locales/en/messages.json @@ -1099,8 +1099,11 @@ "yourVaultIsLockedV2": { "message": "Your vault is locked" }, - "uuid": { - "message": "UUID" + "yourAccountIsLocked": { + "message": "Your account is locked" + }, + "uuid":{ + "message" : "UUID" }, "unlock": { "message": "Unlock" @@ -2564,6 +2567,9 @@ "downloadLicense": { "message": "Download license" }, + "viewBillingToken": { + "message": "View Billing Token" + }, "updateLicense": { "message": "Update license" }, @@ -3169,6 +3175,10 @@ "incorrectPin": { "message": "Incorrect PIN" }, + "pin": { + "message": "PIN", + "description": "PIN code. Ex. The short code (often numeric) that you use to unlock a device." + }, "exportedVault": { "message": "Vault exported" }, @@ -5994,8 +6004,8 @@ "viewBillingSyncToken": { "message": "View billing sync token" }, - "generateBillingSyncToken": { - "message": "Generate billing sync token" + "generateBillingToken": { + "message": "Generate billing token" }, "copyPasteBillingSync": { "message": "Copy and paste this token into the billing sync settings of your self-hosted organization." @@ -6003,8 +6013,8 @@ "billingSyncCanAccess": { "message": "Your billing sync token can access and edit this organization's subscription settings." }, - "manageBillingSync": { - "message": "Manage billing sync" + "manageBillingTokenSync": { + "message": "Manage Billing Token" }, "setUpBillingSync": { "message": "Set up billing sync" @@ -6063,15 +6073,15 @@ "billingSyncApiKeyRotated": { "message": "Token rotated" }, - "billingSyncDesc": { - "message": "Billing sync unlocks Families sponsorships and automatic license syncing on your server. After making updates in the Bitwarden cloud server, select Sync License to apply changes." - }, "billingSyncKeyDesc": { "message": "A billing sync token from your cloud organization's subscription settings is required to complete this form." }, "billingSyncKey": { "message": "Billing sync token" }, + "automaticBillingSyncDesc": { + "message": "Automatic sync unlocks Families sponsorships and allows you to sync your license without uploading a file. After making updates in the Bitwarden cloud server, select Sync License to apply changes." + }, "active": { "message": "Active" }, @@ -6591,6 +6601,18 @@ } } }, + "singleFieldNeedsAttention": { + "message": "1 field needs your attention." + }, + "multipleFieldsNeedAttention": { + "message": "$COUNT$ fields need your attention.", + "placeholders": { + "count": { + "content": "$1", + "example": "2" + } + } + }, "duoHealthCheckResultsInNullAuthUrlError": { "message": "Error connecting with the Duo service. Use a different two-step login method or contact Duo for assistance." }, @@ -7451,6 +7473,15 @@ "or": { "message": "or" }, + "unlockWithBiometrics": { + "message": "Unlock with biometrics" + }, + "unlockWithPin": { + "message": "Unlock with PIN" + }, + "unlockWithMasterPassword": { + "message": "Unlock with master password" + }, "licenseAndBillingManagement": { "message": "License and billing management" }, @@ -7460,11 +7491,11 @@ "manualUpload": { "message": "Manual upload" }, - "manualUploadDesc": { - "message": "If you do not want to opt into billing sync, manually upload your license here." + "manualBillingTokenUploadDesc": { + "message": "If you do not want to opt into billing sync, manually upload your license here. This will not automatically unlock Families sponsorships." }, "syncLicense": { - "message": "Sync license" + "message": "Sync License" }, "licenseSyncSuccess": { "message": "Successfully synced license" diff --git a/apps/web/tsconfig.json b/apps/web/tsconfig.json index 5829e2f6ab6..1e17de148f0 100644 --- a/apps/web/tsconfig.json +++ b/apps/web/tsconfig.json @@ -23,6 +23,7 @@ "@bitwarden/vault-export-ui": ["../../libs/tools/export/vault-export/vault-export-ui/src"], "@bitwarden/importer/core": ["../../libs/importer/src"], "@bitwarden/importer/ui": ["../../libs/importer/src/components"], + "@bitwarden/key-management": ["../../libs/key-management/src"], "@bitwarden/platform": ["../../libs/platform/src"], "@bitwarden/send-ui": ["../../libs/tools/send/send-ui/src"], "@bitwarden/vault": ["../../libs/vault/src"], diff --git a/bitwarden_license/bit-cli/tsconfig.json b/bitwarden_license/bit-cli/tsconfig.json index 4012daac542..bb9986e6c9d 100644 --- a/bitwarden_license/bit-cli/tsconfig.json +++ b/bitwarden_license/bit-cli/tsconfig.json @@ -23,6 +23,7 @@ "@bitwarden/generator-legacy": ["../../libs/tools/generator/extensions/legacy/src"], "@bitwarden/generator-history": ["../../libs/tools/generator/extensions/history/src"], "@bitwarden/generator-navigation": ["../../libs/tools/generator/extensions/navigation/src"], + "@bitwarden/key-management": ["../../libs/key-management/src"], "@bitwarden/vault-export-core": [ "../../libs/tools/export/vault-export/vault-export-core/src" ], diff --git a/bitwarden_license/bit-common/src/admin-console/auth-requests/organization-auth-request.service.spec.ts b/bitwarden_license/bit-common/src/admin-console/auth-requests/organization-auth-request.service.spec.ts index 3ee89cbda51..0eda8aa7655 100644 --- a/bitwarden_license/bit-common/src/admin-console/auth-requests/organization-auth-request.service.spec.ts +++ b/bitwarden_license/bit-common/src/admin-console/auth-requests/organization-auth-request.service.spec.ts @@ -124,8 +124,8 @@ describe("OrganizationAuthRequestService", () => { ); const encryptedUserKey = new EncString("encryptedUserKey"); - cryptoService.rsaDecrypt.mockResolvedValue(new Uint8Array(32)); - cryptoService.rsaEncrypt.mockResolvedValue(encryptedUserKey); + encryptService.rsaDecrypt.mockResolvedValue(new Uint8Array(32)); + encryptService.rsaEncrypt.mockResolvedValue(encryptedUserKey); const mockPendingAuthRequest = new PendingAuthRequestView(); mockPendingAuthRequest.id = "requestId1"; @@ -166,8 +166,8 @@ describe("OrganizationAuthRequestService", () => { ); const encryptedUserKey = new EncString("encryptedUserKey"); - cryptoService.rsaDecrypt.mockResolvedValue(new Uint8Array(32)); - cryptoService.rsaEncrypt.mockResolvedValue(encryptedUserKey); + encryptService.rsaDecrypt.mockResolvedValue(new Uint8Array(32)); + encryptService.rsaEncrypt.mockResolvedValue(encryptedUserKey); const mockPendingAuthRequest = new PendingAuthRequestView(); mockPendingAuthRequest.id = "requestId1"; diff --git a/bitwarden_license/bit-common/src/admin-console/auth-requests/organization-auth-request.service.ts b/bitwarden_license/bit-common/src/admin-console/auth-requests/organization-auth-request.service.ts index ad6e29c5834..9c86b59dfbe 100644 --- a/bitwarden_license/bit-common/src/admin-console/auth-requests/organization-auth-request.service.ts +++ b/bitwarden_license/bit-common/src/admin-console/auth-requests/organization-auth-request.service.ts @@ -117,10 +117,13 @@ export class OrganizationAuthRequestService { ); // Decrypt user key with decrypted org private key - const decValue = await this.cryptoService.rsaDecrypt(encryptedUserKey, decOrgPrivateKey); + const decValue = await this.encryptService.rsaDecrypt( + new EncString(encryptedUserKey), + decOrgPrivateKey, + ); const userKey = new SymmetricCryptoKey(decValue); // Re-encrypt user Key with the Device Public Key - return await this.cryptoService.rsaEncrypt(userKey.key, devicePubKey); + return await this.encryptService.rsaEncrypt(userKey.key, devicePubKey); } } diff --git a/bitwarden_license/bit-common/tsconfig.json b/bitwarden_license/bit-common/tsconfig.json index 21f57001ed7..85ba8cbf60b 100644 --- a/bitwarden_license/bit-common/tsconfig.json +++ b/bitwarden_license/bit-common/tsconfig.json @@ -21,6 +21,7 @@ ], "@bitwarden/vault-export-ui": ["../../libs/tools/export/vault-export/vault-export-core/src"], "@bitwarden/send-ui": ["../../libs/tools/send/send-ui/src"], + "@bitwarden/key-management": ["../../libs/key-management/src"], "@bitwarden/platform": ["../../libs/platform/src"], "@bitwarden/vault": ["../../libs/vault/src"], "@bitwarden/web-vault/*": ["../../apps/web/src/*"], diff --git a/bitwarden_license/bit-web/src/app/admin-console/providers/manage/dialogs/bulk-confirm-dialog.component.ts b/bitwarden_license/bit-web/src/app/admin-console/providers/manage/dialogs/bulk-confirm-dialog.component.ts index 986cf4d30ee..8a04cb6452d 100644 --- a/bitwarden_license/bit-web/src/app/admin-console/providers/manage/dialogs/bulk-confirm-dialog.component.ts +++ b/bitwarden_license/bit-web/src/app/admin-console/providers/manage/dialogs/bulk-confirm-dialog.component.ts @@ -13,6 +13,7 @@ import { ProviderUserBulkPublicKeyResponse } from "@bitwarden/common/admin-conso import { ProviderUserBulkResponse } from "@bitwarden/common/admin-console/models/response/provider/provider-user-bulk.response"; import { ListResponse } from "@bitwarden/common/models/response/list.response"; import { CryptoService } from "@bitwarden/common/platform/abstractions/crypto.service"; +import { EncryptService } from "@bitwarden/common/platform/abstractions/encrypt.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { SymmetricCryptoKey } from "@bitwarden/common/platform/models/domain/symmetric-crypto-key"; import { DialogService } from "@bitwarden/components"; @@ -34,10 +35,11 @@ export class BulkConfirmDialogComponent extends BaseBulkConfirmComponent { constructor( private apiService: ApiService, protected cryptoService: CryptoService, + protected encryptService: EncryptService, @Inject(DIALOG_DATA) protected dialogParams: BulkConfirmDialogParams, protected i18nService: I18nService, ) { - super(cryptoService, i18nService); + super(cryptoService, encryptService, i18nService); this.providerId = dialogParams.providerId; this.users = dialogParams.users; diff --git a/bitwarden_license/bit-web/src/app/admin-console/providers/manage/members.component.ts b/bitwarden_license/bit-web/src/app/admin-console/providers/manage/members.component.ts index f78bccd3548..38c2dade209 100644 --- a/bitwarden_license/bit-web/src/app/admin-console/providers/manage/members.component.ts +++ b/bitwarden_license/bit-web/src/app/admin-console/providers/manage/members.component.ts @@ -15,6 +15,7 @@ import { ProviderUserConfirmRequest } from "@bitwarden/common/admin-console/mode import { ProviderUserUserDetailsResponse } from "@bitwarden/common/admin-console/models/response/provider/provider-user.response"; import { ListResponse } from "@bitwarden/common/models/response/list.response"; import { CryptoService } from "@bitwarden/common/platform/abstractions/crypto.service"; +import { EncryptService } from "@bitwarden/common/platform/abstractions/encrypt.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; import { ValidationService } from "@bitwarden/common/platform/abstractions/validation.service"; @@ -66,6 +67,7 @@ export class MembersComponent extends BaseMembersComponent { toastService: ToastService, userNamePipe: UserNamePipe, validationService: ValidationService, + private encryptService: EncryptService, private activatedRoute: ActivatedRoute, private providerService: ProviderService, private router: Router, @@ -184,7 +186,7 @@ export class MembersComponent extends BaseMembersComponent { async confirmUser(user: ProviderUser, publicKey: Uint8Array): Promise { const providerKey = await this.cryptoService.getProviderKey(this.providerId); - const key = await this.cryptoService.rsaEncrypt(providerKey.key, publicKey); + const key = await this.encryptService.rsaEncrypt(providerKey.key, publicKey); const request = new ProviderUserConfirmRequest(); request.key = key.encryptedString; await this.apiService.postProviderUserConfirm(this.providerId, user.id, request); diff --git a/bitwarden_license/bit-web/src/app/admin-console/providers/manage/people.component.ts b/bitwarden_license/bit-web/src/app/admin-console/providers/manage/people.component.ts index 49961e0c7fc..9293f8c6eb7 100644 --- a/bitwarden_license/bit-web/src/app/admin-console/providers/manage/people.component.ts +++ b/bitwarden_license/bit-web/src/app/admin-console/providers/manage/people.component.ts @@ -17,6 +17,7 @@ import { ProviderUserUserDetailsResponse } from "@bitwarden/common/admin-console import { ListResponse } from "@bitwarden/common/models/response/list.response"; import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; import { CryptoService } from "@bitwarden/common/platform/abstractions/crypto.service"; +import { EncryptService } from "@bitwarden/common/platform/abstractions/encrypt.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; @@ -65,6 +66,7 @@ export class PeopleComponent modalService: ModalService, platformUtilsService: PlatformUtilsService, cryptoService: CryptoService, + private encryptService: EncryptService, private router: Router, searchService: SearchService, validationService: ValidationService, @@ -150,7 +152,7 @@ export class PeopleComponent async confirmUser(user: ProviderUserUserDetailsResponse, publicKey: Uint8Array): Promise { const providerKey = await this.cryptoService.getProviderKey(this.providerId); - const key = await this.cryptoService.rsaEncrypt(providerKey.key, publicKey); + const key = await this.encryptService.rsaEncrypt(providerKey.key, publicKey); const request = new ProviderUserConfirmRequest(); request.key = key.encryptedString; await this.apiService.postProviderUserConfirm(this.providerId, user.id, request); diff --git a/bitwarden_license/bit-web/src/app/admin-console/providers/settings/account.component.html b/bitwarden_license/bit-web/src/app/admin-console/providers/settings/account.component.html index a4e45877552..b6794b2987f 100644 --- a/bitwarden_license/bit-web/src/app/admin-console/providers/settings/account.component.html +++ b/bitwarden_license/bit-web/src/app/admin-console/providers/settings/account.component.html @@ -34,7 +34,7 @@ - + diff --git a/bitwarden_license/bit-web/src/app/admin-console/providers/settings/account.component.ts b/bitwarden_license/bit-web/src/app/admin-console/providers/settings/account.component.ts index d5d7634db4c..0442f04fb72 100644 --- a/bitwarden_license/bit-web/src/app/admin-console/providers/settings/account.component.ts +++ b/bitwarden_license/bit-web/src/app/admin-console/providers/settings/account.component.ts @@ -8,7 +8,6 @@ import { ApiService } from "@bitwarden/common/abstractions/api.service"; import { ProviderApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/provider/provider-api.service.abstraction"; import { ProviderUpdateRequest } from "@bitwarden/common/admin-console/models/request/provider/provider-update.request"; import { ProviderResponse } from "@bitwarden/common/admin-console/models/response/provider/provider.response"; -import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum"; import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; @@ -33,9 +32,6 @@ export class AccountComponent implements OnDestroy, OnInit { providerName: ["" as ProviderResponse["name"]], providerBillingEmail: ["" as ProviderResponse["billingEmail"], Validators.email], }); - protected enableDeleteProvider$ = this.configService.getFeatureFlag$( - FeatureFlag.EnableDeleteProvider, - ); constructor( private apiService: ApiService, diff --git a/bitwarden_license/bit-web/src/app/tools/reports/member-access-report/services/member-access-report.service.ts b/bitwarden_license/bit-web/src/app/tools/reports/member-access-report/services/member-access-report.service.ts index 3616893e231..443edc1d2fc 100644 --- a/bitwarden_license/bit-web/src/app/tools/reports/member-access-report/services/member-access-report.service.ts +++ b/bitwarden_license/bit-web/src/app/tools/reports/member-access-report/services/member-access-report.service.ts @@ -1,9 +1,9 @@ import { Injectable } from "@angular/core"; +import { CollectionAccessSelectionView } from "@bitwarden/admin-console/common"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { EncString } from "@bitwarden/common/platform/models/domain/enc-string"; import { OrganizationId } from "@bitwarden/common/types/guid"; -import { CollectionAccessSelectionView } from "@bitwarden/web-vault/app/admin-console/organizations/core/views"; import { getPermissionList, convertToPermission, diff --git a/bitwarden_license/bit-web/tsconfig.json b/bitwarden_license/bit-web/tsconfig.json index e05ae8018f5..968744d7963 100644 --- a/bitwarden_license/bit-web/tsconfig.json +++ b/bitwarden_license/bit-web/tsconfig.json @@ -23,6 +23,7 @@ "@bitwarden/vault-export-ui": ["../../libs/tools/export/vault-export/vault-export-ui/src"], "@bitwarden/importer/core": ["../../libs/importer/src"], "@bitwarden/importer/ui": ["../../libs/importer/src/components"], + "@bitwarden/key-management": ["../../libs/key-management/src"], "@bitwarden/platform": ["../../libs/platform/src"], "@bitwarden/send-ui": ["../../libs/tools/send/send-ui/src"], "@bitwarden/vault": ["../../libs/vault/src"], diff --git a/libs/admin-console/src/common/collections/abstractions/collection-admin.service.ts b/libs/admin-console/src/common/collections/abstractions/collection-admin.service.ts new file mode 100644 index 00000000000..e5b0bde7ef6 --- /dev/null +++ b/libs/admin-console/src/common/collections/abstractions/collection-admin.service.ts @@ -0,0 +1,16 @@ +import { CollectionDetailsResponse } from "@bitwarden/common/vault/models/response/collection.response"; + +import { CollectionAccessSelectionView, CollectionAdminView } from "../models"; + +export abstract class CollectionAdminService { + getAll: (organizationId: string) => Promise; + get: (organizationId: string, collectionId: string) => Promise; + save: (collection: CollectionAdminView) => Promise; + delete: (organizationId: string, collectionId: string) => Promise; + bulkAssignAccess: ( + organizationId: string, + collectionIds: string[], + users: CollectionAccessSelectionView[], + groups: CollectionAccessSelectionView[], + ) => Promise; +} diff --git a/libs/admin-console/src/common/collections/abstractions/index.ts b/libs/admin-console/src/common/collections/abstractions/index.ts new file mode 100644 index 00000000000..4ee56102061 --- /dev/null +++ b/libs/admin-console/src/common/collections/abstractions/index.ts @@ -0,0 +1 @@ +export * from "./collection-admin.service"; diff --git a/libs/admin-console/src/common/collections/index.ts b/libs/admin-console/src/common/collections/index.ts new file mode 100644 index 00000000000..9187ccd39cf --- /dev/null +++ b/libs/admin-console/src/common/collections/index.ts @@ -0,0 +1,3 @@ +export * from "./abstractions"; +export * from "./models"; +export * from "./services"; diff --git a/apps/web/src/app/vault/core/bulk-collection-access.request.ts b/libs/admin-console/src/common/collections/models/bulk-collection-access.request.ts similarity index 100% rename from apps/web/src/app/vault/core/bulk-collection-access.request.ts rename to libs/admin-console/src/common/collections/models/bulk-collection-access.request.ts diff --git a/apps/web/src/app/admin-console/organizations/core/views/collection-access-selection.view.ts b/libs/admin-console/src/common/collections/models/collection-access-selection.view.ts similarity index 100% rename from apps/web/src/app/admin-console/organizations/core/views/collection-access-selection.view.ts rename to libs/admin-console/src/common/collections/models/collection-access-selection.view.ts diff --git a/apps/web/src/app/vault/core/views/collection-admin.view.ts b/libs/admin-console/src/common/collections/models/collection-admin.view.ts similarity index 92% rename from apps/web/src/app/vault/core/views/collection-admin.view.ts rename to libs/admin-console/src/common/collections/models/collection-admin.view.ts index 10f894505c9..208131a3f71 100644 --- a/apps/web/src/app/vault/core/views/collection-admin.view.ts +++ b/libs/admin-console/src/common/collections/models/collection-admin.view.ts @@ -2,8 +2,9 @@ import { Organization } from "@bitwarden/common/admin-console/models/domain/orga import { CollectionAccessDetailsResponse } from "@bitwarden/common/src/vault/models/response/collection.response"; import { CollectionView } from "@bitwarden/common/vault/models/view/collection.view"; -import { CollectionAccessSelectionView } from "../../../admin-console/organizations/core/views/collection-access-selection.view"; -import { Unassigned } from "../../individual-vault/vault-filter/shared/models/routed-vault-filter.model"; +import { CollectionAccessSelectionView } from "../models"; + +export const Unassigned = "unassigned"; export class CollectionAdminView extends CollectionView { groups: CollectionAccessSelectionView[] = []; diff --git a/libs/admin-console/src/common/collections/models/index.ts b/libs/admin-console/src/common/collections/models/index.ts new file mode 100644 index 00000000000..4f35728b00a --- /dev/null +++ b/libs/admin-console/src/common/collections/models/index.ts @@ -0,0 +1,3 @@ +export * from "./bulk-collection-access.request"; +export * from "./collection-access-selection.view"; +export * from "./collection-admin.view"; diff --git a/apps/web/src/app/vault/core/collection-admin.service.ts b/libs/admin-console/src/common/collections/services/default-collection-admin.service.ts similarity index 94% rename from apps/web/src/app/vault/core/collection-admin.service.ts rename to libs/admin-console/src/common/collections/services/default-collection-admin.service.ts index e0c15e34047..aa2b5bb91d6 100644 --- a/apps/web/src/app/vault/core/collection-admin.service.ts +++ b/libs/admin-console/src/common/collections/services/default-collection-admin.service.ts @@ -1,5 +1,3 @@ -import { Injectable } from "@angular/core"; - import { ApiService } from "@bitwarden/common/abstractions/api.service"; import { SelectionReadOnlyRequest } from "@bitwarden/common/admin-console/models/request/selection-read-only.request"; import { CryptoService } from "@bitwarden/common/platform/abstractions/crypto.service"; @@ -14,13 +12,14 @@ import { CollectionResponse, } from "@bitwarden/common/vault/models/response/collection.response"; -import { CollectionAccessSelectionView } from "../../admin-console/organizations/core"; +import { CollectionAdminService } from "../abstractions"; +import { + BulkCollectionAccessRequest, + CollectionAccessSelectionView, + CollectionAdminView, +} from "../models"; -import { BulkCollectionAccessRequest } from "./bulk-collection-access.request"; -import { CollectionAdminView } from "./views/collection-admin.view"; - -@Injectable() -export class CollectionAdminService { +export class DefaultCollectionAdminService implements CollectionAdminService { constructor( private apiService: ApiService, private cryptoService: CryptoService, diff --git a/libs/admin-console/src/common/collections/services/index.ts b/libs/admin-console/src/common/collections/services/index.ts new file mode 100644 index 00000000000..1e3ed96c6a0 --- /dev/null +++ b/libs/admin-console/src/common/collections/services/index.ts @@ -0,0 +1 @@ +export * from "./default-collection-admin.service"; diff --git a/libs/admin-console/src/common/index.ts b/libs/admin-console/src/common/index.ts index 0af54f8ffbf..edeff5aa314 100644 --- a/libs/admin-console/src/common/index.ts +++ b/libs/admin-console/src/common/index.ts @@ -1 +1,2 @@ export * from "./organization-user"; +export * from "./collections"; diff --git a/libs/admin-console/src/common/organization-user/abstractions/organization-user-api.service.ts b/libs/admin-console/src/common/organization-user/abstractions/organization-user-api.service.ts index ea5d2185ee2..ff7f9c5df6c 100644 --- a/libs/admin-console/src/common/organization-user/abstractions/organization-user-api.service.ts +++ b/libs/admin-console/src/common/organization-user/abstractions/organization-user-api.service.ts @@ -16,6 +16,7 @@ import { OrganizationUserDetailsResponse, OrganizationUserResetPasswordDetailsResponse, OrganizationUserUserDetailsResponse, + OrganizationUserUserMiniResponse, } from "../models/responses"; /** @@ -44,7 +45,9 @@ export abstract class OrganizationUserApiService { abstract getOrganizationUserGroups(organizationId: string, id: string): Promise; /** - * Retrieve a list of all users that belong to the specified organization + * Retrieve full details of all users that belong to the specified organization. + * This is only accessible to privileged users, if you need a simple listing of basic details, use + * {@link getAllMiniUserDetails}. * @param organizationId - Identifier for the organization * @param options - Options for the request */ @@ -56,6 +59,16 @@ export abstract class OrganizationUserApiService { }, ): Promise>; + /** + * Retrieve a list of all users that belong to the specified organization, with basic information only. + * This is suitable for lists of names/emails etc. throughout the app and can be accessed by most users. + * @param organizationId - Identifier for the organization + * @param options - Options for the request + */ + abstract getAllMiniUserDetails( + organizationId: string, + ): Promise>; + /** * Retrieve reset password details for the specified organization user * @param organizationId - Identifier for the user's organization diff --git a/libs/admin-console/src/common/organization-user/models/responses/index.ts b/libs/admin-console/src/common/organization-user/models/responses/index.ts index 29c82fb18b3..aa0a968f71a 100644 --- a/libs/admin-console/src/common/organization-user/models/responses/index.ts +++ b/libs/admin-console/src/common/organization-user/models/responses/index.ts @@ -1,3 +1,4 @@ export * from "./organization-user.response"; export * from "./organization-user-bulk.response"; export * from "./organization-user-bulk-public-key.response"; +export * from "./organization-user-mini.response"; diff --git a/libs/admin-console/src/common/organization-user/models/responses/organization-user-mini.response.ts b/libs/admin-console/src/common/organization-user/models/responses/organization-user-mini.response.ts new file mode 100644 index 00000000000..6ca1bace401 --- /dev/null +++ b/libs/admin-console/src/common/organization-user/models/responses/organization-user-mini.response.ts @@ -0,0 +1,24 @@ +import { + OrganizationUserStatusType, + OrganizationUserType, +} from "@bitwarden/common/admin-console/enums"; +import { BaseResponse } from "@bitwarden/common/models/response/base.response"; + +export class OrganizationUserUserMiniResponse extends BaseResponse { + id: string; + userId: string; + email: string; + name: string; + type: OrganizationUserType; + status: OrganizationUserStatusType; + + constructor(response: any) { + super(response); + this.id = this.getResponseProperty("Id"); + this.userId = this.getResponseProperty("UserId"); + this.email = this.getResponseProperty("Email"); + this.name = this.getResponseProperty("Name"); + this.type = this.getResponseProperty("Type"); + this.status = this.getResponseProperty("Status"); + } +} diff --git a/libs/admin-console/src/common/organization-user/services/default-organization-user-api.service.ts b/libs/admin-console/src/common/organization-user/services/default-organization-user-api.service.ts index 40824550d44..a6438b8b5ff 100644 --- a/libs/admin-console/src/common/organization-user/services/default-organization-user-api.service.ts +++ b/libs/admin-console/src/common/organization-user/services/default-organization-user-api.service.ts @@ -1,5 +1,9 @@ +import { firstValueFrom } from "rxjs"; + import { ApiService } from "@bitwarden/common/abstractions/api.service"; +import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum"; import { ListResponse } from "@bitwarden/common/models/response/list.response"; +import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; import { OrganizationUserApiService } from "../abstractions"; import { @@ -19,10 +23,14 @@ import { OrganizationUserDetailsResponse, OrganizationUserResetPasswordDetailsResponse, OrganizationUserUserDetailsResponse, + OrganizationUserUserMiniResponse, } from "../models/responses"; export class DefaultOrganizationUserApiService implements OrganizationUserApiService { - constructor(private apiService: ApiService) {} + constructor( + private apiService: ApiService, + private configService: ConfigService, + ) {} async getOrganizationUser( organizationId: string, @@ -84,6 +92,27 @@ export class DefaultOrganizationUserApiService implements OrganizationUserApiSer return new ListResponse(r, OrganizationUserUserDetailsResponse); } + async getAllMiniUserDetails( + organizationId: string, + ): Promise> { + const apiEnabled = await firstValueFrom( + this.configService.getFeatureFlag$(FeatureFlag.Pm3478RefactorOrganizationUserApi), + ); + if (!apiEnabled) { + // Keep using the old api until this feature flag is enabled + return this.getAllUsers(organizationId); + } + + const r = await this.apiService.send( + "GET", + `/organizations/${organizationId}/users/mini-details`, + null, + true, + true, + ); + return new ListResponse(r, OrganizationUserUserMiniResponse); + } + async getOrganizationUserResetPasswordDetails( organizationId: string, id: string, diff --git a/libs/angular/src/auth/components/lock.component.ts b/libs/angular/src/auth/components/lock.component.ts index 484e1c63469..5fc8f51d575 100644 --- a/libs/angular/src/auth/components/lock.component.ts +++ b/libs/angular/src/auth/components/lock.component.ts @@ -29,14 +29,13 @@ import { LogService } from "@bitwarden/common/platform/abstractions/log.service" import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service"; import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; import { StateService } from "@bitwarden/common/platform/abstractions/state.service"; -import { BiometricStateService } from "@bitwarden/common/platform/biometrics/biometric-state.service"; -import { BiometricsService } from "@bitwarden/common/platform/biometrics/biometric.service"; import { KeySuffixOptions } from "@bitwarden/common/platform/enums"; import { PasswordStrengthServiceAbstraction } from "@bitwarden/common/tools/password-strength"; import { UserId } from "@bitwarden/common/types/guid"; import { UserKey } from "@bitwarden/common/types/key"; import { SyncService } from "@bitwarden/common/vault/abstractions/sync/sync.service.abstraction"; import { DialogService, ToastService } from "@bitwarden/components"; +import { BiometricStateService, BiometricsService } from "@bitwarden/key-management"; @Directive() export class LockComponent implements OnInit, OnDestroy { diff --git a/libs/angular/src/auth/components/login.component.ts b/libs/angular/src/auth/components/login.component.ts index 831d505a388..1c8f4f656e1 100644 --- a/libs/angular/src/auth/components/login.component.ts +++ b/libs/angular/src/auth/components/login.component.ts @@ -1,6 +1,6 @@ import { Directive, ElementRef, NgZone, OnDestroy, OnInit, ViewChild } from "@angular/core"; import { FormBuilder, Validators } from "@angular/forms"; -import { ActivatedRoute, Router } from "@angular/router"; +import { ActivatedRoute, NavigationSkipped, Router } from "@angular/router"; import { Subject, firstValueFrom, of } from "rxjs"; import { switchMap, take, takeUntil } from "rxjs/operators"; @@ -121,6 +121,14 @@ export class LoginComponent extends CaptchaProtectedComponent implements OnInit, ) .subscribe(); + // If the user navigates to /login from /login, reset the validatedEmail flag + // This should bring the user back to the login screen with the email field + this.router.events.pipe(takeUntil(this.destroy$)).subscribe((event) => { + if (event instanceof NavigationSkipped && event.url === "/login") { + this.validatedEmail = false; + } + }); + // Backup check to handle unknown case where activatedRoute is not available // This shouldn't happen under normal circumstances if (!this.route) { diff --git a/libs/angular/src/auth/components/set-password.component.ts b/libs/angular/src/auth/components/set-password.component.ts index ea4c2fb9267..d1a21e4abb2 100644 --- a/libs/angular/src/auth/components/set-password.component.ts +++ b/libs/angular/src/auth/components/set-password.component.ts @@ -215,7 +215,7 @@ export class SetPasswordComponent extends BaseChangePasswordComponent implements // RSA Encrypt user key with organization public key const userKey = await this.cryptoService.getUserKey(); - const encryptedUserKey = await this.cryptoService.rsaEncrypt(userKey.key, publicKey); + const encryptedUserKey = await this.encryptService.rsaEncrypt(userKey.key, publicKey); const resetRequest = new OrganizationUserResetPasswordEnrollmentRequest(); resetRequest.masterPasswordHash = masterPasswordHash; diff --git a/libs/angular/src/auth/guards/auth.guard.ts b/libs/angular/src/auth/guards/auth.guard.ts index b54f114d3d4..1486b9b57d8 100644 --- a/libs/angular/src/auth/guards/auth.guard.ts +++ b/libs/angular/src/auth/guards/auth.guard.ts @@ -38,6 +38,8 @@ export const authGuard: CanActivateFn = async ( if (routerState != null) { messagingService.send("lockedUrl", { url: routerState.url }); } + // TODO PM-9674: when extension refresh is finished, remove promptBiometric + // as it has been integrated into the component as a default feature. return router.createUrlTree(["lock"], { queryParams: { promptBiometric: true } }); } diff --git a/libs/angular/src/services/jslib-services.module.ts b/libs/angular/src/services/jslib-services.module.ts index 734ae03d59b..512f0730f8f 100644 --- a/libs/angular/src/services/jslib-services.module.ts +++ b/libs/angular/src/services/jslib-services.module.ts @@ -151,10 +151,6 @@ import { PlatformUtilsService as PlatformUtilsServiceAbstraction } from "@bitwar import { StateService as StateServiceAbstraction } from "@bitwarden/common/platform/abstractions/state.service"; import { AbstractStorageService } from "@bitwarden/common/platform/abstractions/storage.service"; import { ValidationService as ValidationServiceAbstraction } from "@bitwarden/common/platform/abstractions/validation.service"; -import { - BiometricStateService, - DefaultBiometricStateService, -} from "@bitwarden/common/platform/biometrics/biometric-state.service"; import { StateFactory } from "@bitwarden/common/platform/factories/state-factory"; import { Message, MessageListener, MessageSender } from "@bitwarden/common/platform/messaging"; // eslint-disable-next-line no-restricted-imports -- Used for dependency injection @@ -263,6 +259,7 @@ import { ImportService, ImportServiceAbstraction, } from "@bitwarden/importer/core"; +import { BiometricStateService, DefaultBiometricStateService } from "@bitwarden/key-management"; import { PasswordRepromptService } from "@bitwarden/vault"; import { VaultExportService, @@ -955,7 +952,7 @@ const safeProviders: SafeProvider[] = [ safeProvider({ provide: OrganizationUserApiService, useClass: DefaultOrganizationUserApiService, - deps: [ApiServiceAbstraction], + deps: [ApiServiceAbstraction, ConfigService], }), safeProvider({ provide: PasswordResetEnrollmentServiceAbstraction, @@ -964,6 +961,7 @@ const safeProviders: SafeProvider[] = [ OrganizationApiServiceAbstraction, AccountServiceAbstraction, CryptoServiceAbstraction, + EncryptService, OrganizationUserApiService, I18nServiceAbstraction, ], @@ -1093,6 +1091,7 @@ const safeProviders: SafeProvider[] = [ AccountServiceAbstraction, InternalMasterPasswordServiceAbstraction, CryptoServiceAbstraction, + EncryptService, ApiServiceAbstraction, StateProvider, ], @@ -1288,6 +1287,7 @@ const safeProviders: SafeProvider[] = [ deps: [ ApiServiceAbstraction, CryptoServiceAbstraction, + EncryptService, I18nServiceAbstraction, KdfConfigServiceAbstraction, InternalMasterPasswordServiceAbstraction, diff --git a/libs/angular/src/vault/components/add-edit.component.ts b/libs/angular/src/vault/components/add-edit.component.ts index 21a7b35ac51..960a226b1cf 100644 --- a/libs/angular/src/vault/components/add-edit.component.ts +++ b/libs/angular/src/vault/components/add-edit.component.ts @@ -308,9 +308,7 @@ export class AddEditComponent implements OnInit, OnDestroy { this.folders$ = this.folderService.folderViews$; if (this.editMode && this.previousCipherId !== this.cipherId) { - // FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling. - // eslint-disable-next-line @typescript-eslint/no-floating-promises - this.eventCollectionService.collect(EventType.Cipher_ClientViewed, this.cipherId); + void this.eventCollectionService.collectMany(EventType.Cipher_ClientViewed, [this.cipher]); } this.previousCipherId = this.cipherId; this.reprompt = this.cipher.reprompt !== CipherRepromptType.None; @@ -551,12 +549,9 @@ export class AddEditComponent implements OnInit, OnDestroy { if (this.editMode && this.showPassword) { document.getElementById("loginPassword")?.focus(); - // FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling. - // eslint-disable-next-line @typescript-eslint/no-floating-promises - this.eventCollectionService.collect( - EventType.Cipher_ClientToggledPasswordVisible, - this.cipherId, - ); + void this.eventCollectionService.collectMany(EventType.Cipher_ClientToggledPasswordVisible, [ + this.cipher, + ]); } } @@ -566,23 +561,18 @@ export class AddEditComponent implements OnInit, OnDestroy { if (this.editMode && this.showTotpSeed) { document.getElementById("loginTotp")?.focus(); - // FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling. - // eslint-disable-next-line @typescript-eslint/no-floating-promises - this.eventCollectionService.collect( - EventType.Cipher_ClientToggledTOTPSeedVisible, - this.cipherId, - ); + void this.eventCollectionService.collectMany(EventType.Cipher_ClientToggledTOTPSeedVisible, [ + this.cipher, + ]); } } async toggleCardNumber() { this.showCardNumber = !this.showCardNumber; if (this.showCardNumber) { - // FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling. - // eslint-disable-next-line @typescript-eslint/no-floating-promises - this.eventCollectionService.collect( + void this.eventCollectionService.collectMany( EventType.Cipher_ClientToggledCardNumberVisible, - this.cipherId, + [this.cipher], ); } } @@ -591,12 +581,9 @@ export class AddEditComponent implements OnInit, OnDestroy { this.showCardCode = !this.showCardCode; document.getElementById("cardCode").focus(); if (this.editMode && this.showCardCode) { - // FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling. - // eslint-disable-next-line @typescript-eslint/no-floating-promises - this.eventCollectionService.collect( - EventType.Cipher_ClientToggledCardCodeVisible, - this.cipherId, - ); + void this.eventCollectionService.collectMany(EventType.Cipher_ClientToggledCardCodeVisible, [ + this.cipher, + ]); } } @@ -742,17 +729,17 @@ export class AddEditComponent implements OnInit, OnDestroy { ); if (typeI18nKey === "password") { - // FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling. - // eslint-disable-next-line @typescript-eslint/no-floating-promises - this.eventCollectionService.collect(EventType.Cipher_ClientCopiedPassword, this.cipherId); + void this.eventCollectionService.collectMany(EventType.Cipher_ClientCopiedPassword, [ + this.cipher, + ]); } else if (typeI18nKey === "securityCode") { - // FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling. - // eslint-disable-next-line @typescript-eslint/no-floating-promises - this.eventCollectionService.collect(EventType.Cipher_ClientCopiedCardCode, this.cipherId); + void this.eventCollectionService.collectMany(EventType.Cipher_ClientCopiedCardCode, [ + this.cipher, + ]); } else if (aType === "H_Field") { - // FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling. - // eslint-disable-next-line @typescript-eslint/no-floating-promises - this.eventCollectionService.collect(EventType.Cipher_ClientCopiedHiddenField, this.cipherId); + void this.eventCollectionService.collectMany(EventType.Cipher_ClientCopiedHiddenField, [ + this.cipher, + ]); } return true; diff --git a/libs/auth/src/angular/anon-layout/anon-layout.component.html b/libs/auth/src/angular/anon-layout/anon-layout.component.html index 9e6c27f6016..aa5ff029652 100644 --- a/libs/auth/src/angular/anon-layout/anon-layout.component.html +++ b/libs/auth/src/angular/anon-layout/anon-layout.component.html @@ -9,7 +9,9 @@ 'tw-min-h-[calc(100vh-54px)]': clientType === 'desktop', }" > - + + +
diff --git a/libs/auth/src/angular/anon-layout/anon-layout.component.ts b/libs/auth/src/angular/anon-layout/anon-layout.component.ts index a40fafc5db9..ff28f24a936 100644 --- a/libs/auth/src/angular/anon-layout/anon-layout.component.ts +++ b/libs/auth/src/angular/anon-layout/anon-layout.component.ts @@ -1,5 +1,6 @@ import { CommonModule } from "@angular/common"; import { Component, Input, OnChanges, OnInit, SimpleChanges } from "@angular/core"; +import { RouterModule } from "@angular/router"; import { firstValueFrom } from "rxjs"; import { ClientType } from "@bitwarden/common/enums"; @@ -15,7 +16,7 @@ import { BitwardenLogo, BitwardenShield } from "../icons"; standalone: true, selector: "auth-anon-layout", templateUrl: "./anon-layout.component.html", - imports: [IconModule, CommonModule, TypographyModule, SharedModule], + imports: [IconModule, CommonModule, TypographyModule, SharedModule, RouterModule], }) export class AnonLayoutComponent implements OnInit, OnChanges { @Input() title: string; diff --git a/libs/auth/src/angular/index.ts b/libs/auth/src/angular/index.ts index bfb3a67aedc..6de473c33e7 100644 --- a/libs/auth/src/angular/index.ts +++ b/libs/auth/src/angular/index.ts @@ -43,5 +43,9 @@ export * from "./registration/registration-env-selector/registration-env-selecto export * from "./registration/registration-finish/registration-finish.service"; export * from "./registration/registration-finish/default-registration-finish.service"; +// lock +export * from "./lock/lock.component"; +export * from "./lock/lock-component.service"; + // vault timeout export * from "./vault-timeout-input/vault-timeout-input.component"; diff --git a/libs/auth/src/angular/lock/lock-component.service.ts b/libs/auth/src/angular/lock/lock-component.service.ts new file mode 100644 index 00000000000..fe54db21baa --- /dev/null +++ b/libs/auth/src/angular/lock/lock-component.service.ts @@ -0,0 +1,48 @@ +import { Observable } from "rxjs"; + +import { UserId } from "@bitwarden/common/types/guid"; + +export enum BiometricsDisableReason { + NotSupportedOnOperatingSystem = "NotSupportedOnOperatingSystem", + EncryptedKeysUnavailable = "BiometricsEncryptedKeysUnavailable", + SystemBiometricsUnavailable = "SystemBiometricsUnavailable", +} + +// ex: type UnlockOptionValue = "masterPassword" | "pin" | "biometrics" +export type UnlockOptionValue = (typeof UnlockOption)[keyof typeof UnlockOption]; + +export const UnlockOption = Object.freeze({ + MasterPassword: "masterPassword", + Pin: "pin", + Biometrics: "biometrics", +}) satisfies { [Prop in keyof UnlockOptions as Capitalize]: Prop }; + +export type UnlockOptions = { + masterPassword: { + enabled: boolean; + }; + pin: { + enabled: boolean; + }; + biometrics: { + enabled: boolean; + disableReason: BiometricsDisableReason | null; + }; +}; + +/** + * The LockComponentService is a service which allows the single libs/auth LockComponent to delegate all + * client specific functionality to client specific services implementations of LockComponentService. + */ +export abstract class LockComponentService { + // Extension + abstract getBiometricsError(error: any): string | null; + abstract getPreviousUrl(): string | null; + + // Desktop only + abstract isWindowVisible(): Promise; + abstract getBiometricsUnlockBtnText(): string; + + // Multi client + abstract getAvailableUnlockOptions$(userId: UserId): Observable; +} diff --git a/libs/auth/src/angular/lock/lock.component.html b/libs/auth/src/angular/lock/lock.component.html new file mode 100644 index 00000000000..5f5991c681e --- /dev/null +++ b/libs/auth/src/angular/lock/lock.component.html @@ -0,0 +1,191 @@ + +
+ +
+
+ + + + + + +
+

{{ "or" | i18n }}

+ + + + + + + + + + +
+
+ + + +
+ + {{ "pin" | i18n }} + + + + +
+ + +

{{ "or" | i18n }}

+ + + + + + + + + + +
+
+
+ + + +
+ + {{ "masterPass" | i18n }} + + + + + + +
+ + +

{{ "or" | i18n }}

+ + + + + + + + + + +
+
+
+
diff --git a/libs/auth/src/angular/lock/lock.component.ts b/libs/auth/src/angular/lock/lock.component.ts new file mode 100644 index 00000000000..7bea14f221e --- /dev/null +++ b/libs/auth/src/angular/lock/lock.component.ts @@ -0,0 +1,638 @@ +import { CommonModule } from "@angular/common"; +import { Component, NgZone, OnDestroy, OnInit } from "@angular/core"; +import { FormBuilder, FormGroup, ReactiveFormsModule, Validators } from "@angular/forms"; +import { Router } from "@angular/router"; +import { BehaviorSubject, firstValueFrom, Subject, switchMap, take, takeUntil } from "rxjs"; + +import { JslibModule } from "@bitwarden/angular/jslib.module"; +import { InternalPolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction"; +import { MasterPasswordPolicyOptions } from "@bitwarden/common/admin-console/models/domain/master-password-policy-options"; +import { AccountInfo, AccountService } from "@bitwarden/common/auth/abstractions/account.service"; +import { DeviceTrustServiceAbstraction } from "@bitwarden/common/auth/abstractions/device-trust.service.abstraction"; +import { InternalMasterPasswordServiceAbstraction } from "@bitwarden/common/auth/abstractions/master-password.service.abstraction"; +import { UserVerificationService } from "@bitwarden/common/auth/abstractions/user-verification/user-verification.service.abstraction"; +import { VerificationType } from "@bitwarden/common/auth/enums/verification-type"; +import { ForceSetPasswordReason } from "@bitwarden/common/auth/models/domain/force-set-password-reason"; +import { + MasterPasswordVerification, + MasterPasswordVerificationResponse, +} from "@bitwarden/common/auth/types/verification"; +import { ClientType } from "@bitwarden/common/enums"; +import { BroadcasterService } from "@bitwarden/common/platform/abstractions/broadcaster.service"; +import { CryptoService } from "@bitwarden/common/platform/abstractions/crypto.service"; +import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; +import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; +import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service"; +import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; +import { KeySuffixOptions } from "@bitwarden/common/platform/enums"; +import { SyncService } from "@bitwarden/common/platform/sync"; +import { PasswordStrengthServiceAbstraction } from "@bitwarden/common/tools/password-strength"; +import { UserId } from "@bitwarden/common/types/guid"; +import { UserKey } from "@bitwarden/common/types/key"; +import { + AsyncActionsModule, + ButtonModule, + DialogService, + FormFieldModule, + IconButtonModule, + ToastService, +} from "@bitwarden/components"; +import { BiometricStateService } from "@bitwarden/key-management"; + +import { PinServiceAbstraction } from "../../common/abstractions"; +import { AnonLayoutWrapperDataService } from "../anon-layout/anon-layout-wrapper-data.service"; + +import { + UnlockOption, + LockComponentService, + UnlockOptions, + UnlockOptionValue, +} from "./lock-component.service"; + +const BroadcasterSubscriptionId = "LockComponent"; + +const clientTypeToSuccessRouteRecord: Partial> = { + [ClientType.Web]: "vault", + [ClientType.Desktop]: "vault", + [ClientType.Browser]: "/tabs/current", +}; + +@Component({ + selector: "bit-lock", + templateUrl: "lock.component.html", + standalone: true, + imports: [ + CommonModule, + JslibModule, + ReactiveFormsModule, + ButtonModule, + FormFieldModule, + AsyncActionsModule, + IconButtonModule, + ], +}) +export class LockV2Component implements OnInit, OnDestroy { + private destroy$ = new Subject(); + + activeAccount: { id: UserId | undefined } & AccountInfo; + + clientType: ClientType; + ClientType = ClientType; + + unlockOptions: UnlockOptions = null; + + UnlockOption = UnlockOption; + + private _activeUnlockOptionBSubject: BehaviorSubject = + new BehaviorSubject(null); + + activeUnlockOption$ = this._activeUnlockOptionBSubject.asObservable(); + + set activeUnlockOption(value: UnlockOptionValue) { + this._activeUnlockOptionBSubject.next(value); + } + + get activeUnlockOption(): UnlockOptionValue { + return this._activeUnlockOptionBSubject.value; + } + + private invalidPinAttempts = 0; + + biometricUnlockBtnText: string; + + // masterPassword = ""; + showPassword = false; + private enforcedMasterPasswordOptions: MasterPasswordPolicyOptions = undefined; + + forcePasswordResetRoute = "update-temp-password"; + + formGroup: FormGroup; + + // Desktop properties: + private deferFocus: boolean = null; + private biometricAsked = false; + + // Browser extension properties: + private isInitialLockScreen = (window as any).previousPopupUrl == null; + + defaultUnlockOptionSetForUser = false; + + unlockingViaBiometrics = false; + + constructor( + private accountService: AccountService, + private pinService: PinServiceAbstraction, + private userVerificationService: UserVerificationService, + private cryptoService: CryptoService, + private platformUtilsService: PlatformUtilsService, + private router: Router, + private dialogService: DialogService, + private messagingService: MessagingService, + private biometricStateService: BiometricStateService, + private ngZone: NgZone, + private i18nService: I18nService, + private masterPasswordService: InternalMasterPasswordServiceAbstraction, + private logService: LogService, + private deviceTrustService: DeviceTrustServiceAbstraction, + private syncService: SyncService, + private policyService: InternalPolicyService, + private passwordStrengthService: PasswordStrengthServiceAbstraction, + private formBuilder: FormBuilder, + private toastService: ToastService, + + private lockComponentService: LockComponentService, + private anonLayoutWrapperDataService: AnonLayoutWrapperDataService, + + // desktop deps + private broadcasterService: BroadcasterService, + ) {} + + async ngOnInit() { + this.listenForActiveUnlockOptionChanges(); + + // Listen for active account changes + this.listenForActiveAccountChanges(); + + // Identify client + this.clientType = this.platformUtilsService.getClientType(); + + if (this.clientType === "desktop") { + await this.desktopOnInit(); + } + } + + // Base component methods + private listenForActiveUnlockOptionChanges() { + this.activeUnlockOption$ + .pipe(takeUntil(this.destroy$)) + .subscribe((activeUnlockOption: UnlockOptionValue) => { + if (activeUnlockOption === UnlockOption.Pin) { + this.buildPinForm(); + } else if (activeUnlockOption === UnlockOption.MasterPassword) { + this.buildMasterPasswordForm(); + } + }); + } + + private buildMasterPasswordForm() { + this.formGroup = this.formBuilder.group( + { + masterPassword: ["", [Validators.required]], + }, + { updateOn: "submit" }, + ); + } + + private buildPinForm() { + this.formGroup = this.formBuilder.group( + { + pin: ["", [Validators.required]], + }, + { updateOn: "submit" }, + ); + } + + private listenForActiveAccountChanges() { + this.accountService.activeAccount$ + .pipe( + switchMap((account) => { + return this.handleActiveAccountChange(account); + }), + takeUntil(this.destroy$), + ) + .subscribe(); + } + + private async handleActiveAccountChange(activeAccount: { id: UserId | undefined } & AccountInfo) { + this.activeAccount = activeAccount; + + this.resetDataOnActiveAccountChange(); + + this.setEmailAsPageSubtitle(activeAccount.email); + + this.unlockOptions = await firstValueFrom( + this.lockComponentService.getAvailableUnlockOptions$(activeAccount.id), + ); + + this.setDefaultActiveUnlockOption(this.unlockOptions); + + if (this.unlockOptions.biometrics.enabled) { + await this.handleBiometricsUnlockEnabled(); + } + } + + private resetDataOnActiveAccountChange() { + this.defaultUnlockOptionSetForUser = false; + this.unlockOptions = null; + this.activeUnlockOption = null; + this.formGroup = null; // new form group will be created based on new active unlock option + + // Desktop properties: + this.biometricAsked = false; + } + + private setEmailAsPageSubtitle(email: string) { + this.anonLayoutWrapperDataService.setAnonLayoutWrapperData({ + pageSubtitle: { + subtitle: email, + translate: false, + }, + }); + } + + private setDefaultActiveUnlockOption(unlockOptions: UnlockOptions) { + // Priorities should be Biometrics > Pin > Master Password for speed + if (unlockOptions.biometrics.enabled) { + this.activeUnlockOption = UnlockOption.Biometrics; + } else if (unlockOptions.pin.enabled) { + this.activeUnlockOption = UnlockOption.Pin; + } else if (unlockOptions.masterPassword.enabled) { + this.activeUnlockOption = UnlockOption.MasterPassword; + } + } + + private async handleBiometricsUnlockEnabled() { + this.biometricUnlockBtnText = this.lockComponentService.getBiometricsUnlockBtnText(); + + const autoPromptBiometrics = await firstValueFrom( + this.biometricStateService.promptAutomatically$, + ); + + // TODO: PM-12546 - we need to make our biometric autoprompt experience consistent between the + // desktop and extension. + if (this.clientType === "desktop") { + if (autoPromptBiometrics) { + await this.desktopAutoPromptBiometrics(); + } + } + + if (this.clientType === "browser") { + if ( + this.unlockOptions.biometrics.enabled && + autoPromptBiometrics && + this.isInitialLockScreen // only autoprompt biometrics on initial lock screen + ) { + await this.unlockViaBiometrics(); + } + } + } + + // Note: this submit method is only used for unlock methods that require a form and user input. + // For biometrics unlock, the method is called directly. + submit = async (): Promise => { + if (this.activeUnlockOption === UnlockOption.Pin) { + return await this.unlockViaPin(); + } + + await this.unlockViaMasterPassword(); + }; + + async logOut() { + const confirmed = await this.dialogService.openSimpleDialog({ + title: { key: "logOut" }, + content: { key: "logOutConfirmation" }, + acceptButtonText: { key: "logOut" }, + type: "warning", + }); + + if (confirmed) { + this.messagingService.send("logout", { userId: this.activeAccount.id }); + } + } + + async unlockViaBiometrics(): Promise { + this.unlockingViaBiometrics = true; + + if (!this.unlockOptions.biometrics.enabled) { + this.unlockingViaBiometrics = false; + return; + } + + try { + await this.biometricStateService.setUserPromptCancelled(); + const userKey = await this.cryptoService.getUserKeyFromStorage( + KeySuffixOptions.Biometric, + this.activeAccount.id, + ); + + // If user cancels biometric prompt, userKey is undefined. + if (userKey) { + await this.setUserKeyAndContinue(userKey, false); + } + + this.unlockingViaBiometrics = false; + } catch (e) { + // Cancelling is a valid action. + if (e?.message === "canceled") { + this.unlockingViaBiometrics = false; + return; + } + + let biometricTranslatedErrorDesc; + + if (this.clientType === "browser") { + const biometricErrorDescTranslationKey = this.lockComponentService.getBiometricsError(e); + + if (biometricErrorDescTranslationKey) { + biometricTranslatedErrorDesc = this.i18nService.t(biometricErrorDescTranslationKey); + } + } + + // if no translation key found, show generic error message + if (!biometricTranslatedErrorDesc) { + biometricTranslatedErrorDesc = this.i18nService.t("unexpectedError"); + } + + const confirmed = await this.dialogService.openSimpleDialog({ + title: { key: "error" }, + content: biometricTranslatedErrorDesc, + acceptButtonText: { key: "tryAgain" }, + type: "danger", + }); + + if (confirmed) { + // try again + await this.unlockViaBiometrics(); + } + + this.unlockingViaBiometrics = false; + } + } + + togglePassword() { + this.showPassword = !this.showPassword; + const input = document.getElementById( + this.unlockOptions.pin.enabled ? "pin" : "masterPassword", + ); + if (this.ngZone.isStable) { + input.focus(); + } else { + // eslint-disable-next-line rxjs-angular/prefer-takeuntil + this.ngZone.onStable.pipe(take(1)).subscribe(() => input.focus()); + } + } + + private validatePin(): boolean { + if (this.formGroup.invalid) { + this.toastService.showToast({ + variant: "error", + title: this.i18nService.t("errorOccurred"), + message: this.i18nService.t("pinRequired"), + }); + return false; + } + + return true; + } + + private async unlockViaPin() { + if (!this.validatePin()) { + return; + } + + const pin = this.formGroup.controls.pin.value; + + const MAX_INVALID_PIN_ENTRY_ATTEMPTS = 5; + + try { + const userKey = await this.pinService.decryptUserKeyWithPin(pin, this.activeAccount.id); + + if (userKey) { + await this.setUserKeyAndContinue(userKey); + return; // successfully unlocked + } + + // Failure state: invalid PIN or failed decryption + this.invalidPinAttempts++; + + // Log user out if they have entered an invalid PIN too many times + if (this.invalidPinAttempts >= MAX_INVALID_PIN_ENTRY_ATTEMPTS) { + this.toastService.showToast({ + variant: "error", + title: null, + message: this.i18nService.t("tooManyInvalidPinEntryAttemptsLoggingOut"), + }); + this.messagingService.send("logout"); + return; + } + + this.toastService.showToast({ + variant: "error", + title: this.i18nService.t("errorOccurred"), + message: this.i18nService.t("invalidPin"), + }); + } catch { + this.toastService.showToast({ + variant: "error", + title: this.i18nService.t("errorOccurred"), + message: this.i18nService.t("unexpectedError"), + }); + } + } + + private validateMasterPassword(): boolean { + if (this.formGroup.invalid) { + this.toastService.showToast({ + variant: "error", + title: this.i18nService.t("errorOccurred"), + message: this.i18nService.t("masterPasswordRequired"), + }); + return false; + } + + return true; + } + + private async unlockViaMasterPassword() { + if (!this.validateMasterPassword()) { + return; + } + + const masterPassword = this.formGroup.controls.masterPassword.value; + + const verification = { + type: VerificationType.MasterPassword, + secret: masterPassword, + } as MasterPasswordVerification; + + let passwordValid = false; + let masterPasswordVerificationResponse: MasterPasswordVerificationResponse; + try { + masterPasswordVerificationResponse = + await this.userVerificationService.verifyUserByMasterPassword( + verification, + this.activeAccount.id, + this.activeAccount.email, + ); + + this.enforcedMasterPasswordOptions = MasterPasswordPolicyOptions.fromResponse( + masterPasswordVerificationResponse.policyOptions, + ); + passwordValid = true; + } catch (e) { + this.logService.error(e); + } + + if (!passwordValid) { + this.toastService.showToast({ + variant: "error", + title: this.i18nService.t("errorOccurred"), + message: this.i18nService.t("invalidMasterPassword"), + }); + return; + } + + const userKey = await this.masterPasswordService.decryptUserKeyWithMasterKey( + masterPasswordVerificationResponse.masterKey, + ); + await this.setUserKeyAndContinue(userKey, true); + } + + private async setUserKeyAndContinue(key: UserKey, evaluatePasswordAfterUnlock = false) { + await this.cryptoService.setUserKey(key, this.activeAccount.id); + + // Now that we have a decrypted user key in memory, we can check if we + // need to establish trust on the current device + await this.deviceTrustService.trustDeviceIfRequired(this.activeAccount.id); + + await this.doContinue(evaluatePasswordAfterUnlock); + } + + private async doContinue(evaluatePasswordAfterUnlock: boolean) { + await this.biometricStateService.resetUserPromptCancelled(); + this.messagingService.send("unlocked"); + + if (evaluatePasswordAfterUnlock) { + try { + // If we do not have any saved policies, attempt to load them from the service + if (this.enforcedMasterPasswordOptions == undefined) { + this.enforcedMasterPasswordOptions = await firstValueFrom( + this.policyService.masterPasswordPolicyOptions$(), + ); + } + + if (this.requirePasswordChange()) { + const userId = (await firstValueFrom(this.accountService.activeAccount$))?.id; + await this.masterPasswordService.setForceSetPasswordReason( + ForceSetPasswordReason.WeakMasterPassword, + userId, + ); + await this.router.navigate([this.forcePasswordResetRoute]); + return; + } + } catch (e) { + // Do not prevent unlock if there is an error evaluating policies + this.logService.error(e); + } + } + + // Vault can be de-synced since notifications get ignored while locked. Need to check whether sync is required using the sync service. + await this.syncService.fullSync(false); + + if (this.clientType === "browser") { + const previousUrl = this.lockComponentService.getPreviousUrl(); + if (previousUrl) { + await this.router.navigateByUrl(previousUrl); + } + } + + // determine success route based on client type + const successRoute = clientTypeToSuccessRouteRecord[this.clientType]; + await this.router.navigate([successRoute]); + } + + /** + * Checks if the master password meets the enforced policy requirements + * If not, returns false + */ + private requirePasswordChange(): boolean { + if ( + this.enforcedMasterPasswordOptions == undefined || + !this.enforcedMasterPasswordOptions.enforceOnLogin + ) { + return false; + } + + const masterPassword = this.formGroup.controls.masterPassword.value; + + const passwordStrength = this.passwordStrengthService.getPasswordStrength( + masterPassword, + this.activeAccount.email, + )?.score; + + return !this.policyService.evaluateMasterPassword( + passwordStrength, + masterPassword, + this.enforcedMasterPasswordOptions, + ); + } + + // ----------------------------------------------------------------------------------------------- + // Desktop methods: + // ----------------------------------------------------------------------------------------------- + + async desktopOnInit() { + // TODO: move this into a WindowService and subscribe to messages via MessageListener service. + this.broadcasterService.subscribe(BroadcasterSubscriptionId, async (message: any) => { + this.ngZone.run(() => { + switch (message.command) { + case "windowHidden": + this.onWindowHidden(); + break; + case "windowIsFocused": + if (this.deferFocus === null) { + this.deferFocus = !message.windowIsFocused; + if (!this.deferFocus) { + this.focusInput(); + } + } else if (this.deferFocus && message.windowIsFocused) { + this.focusInput(); + this.deferFocus = false; + } + break; + default: + } + }); + }); + this.messagingService.send("getWindowIsFocused"); + } + + private async desktopAutoPromptBiometrics() { + if (!this.unlockOptions?.biometrics?.enabled || this.biometricAsked) { + return; + } + + // prevent the biometric prompt from showing if the user has already cancelled it + if (await firstValueFrom(this.biometricStateService.promptCancelled$)) { + return; + } + + const windowVisible = await this.lockComponentService.isWindowVisible(); + + if (windowVisible) { + this.biometricAsked = true; + await this.unlockViaBiometrics(); + } + } + + onWindowHidden() { + this.showPassword = false; + } + + private focusInput() { + if (this.unlockOptions) { + document.getElementById(this.unlockOptions.pin.enabled ? "pin" : "masterPassword")?.focus(); + } + } + + // ----------------------------------------------------------------------------------------------- + + ngOnDestroy() { + this.destroy$.next(); + this.destroy$.complete(); + + if (this.clientType === "desktop") { + this.broadcasterService.unsubscribe(BroadcasterSubscriptionId); + } + } +} diff --git a/libs/auth/src/angular/set-password-jit/default-set-password-jit.service.spec.ts b/libs/auth/src/angular/set-password-jit/default-set-password-jit.service.spec.ts index f47e217d0ec..f36283e0c06 100644 --- a/libs/auth/src/angular/set-password-jit/default-set-password-jit.service.spec.ts +++ b/libs/auth/src/angular/set-password-jit/default-set-password-jit.service.spec.ts @@ -15,6 +15,7 @@ import { DEFAULT_KDF_CONFIG } from "@bitwarden/common/auth/models/domain/kdf-con import { SetPasswordRequest } from "@bitwarden/common/auth/models/request/set-password.request"; import { KeysRequest } from "@bitwarden/common/models/request/keys.request"; import { CryptoService } from "@bitwarden/common/platform/abstractions/crypto.service"; +import { EncryptService } from "@bitwarden/common/platform/abstractions/encrypt.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { Utils } from "@bitwarden/common/platform/misc/utils"; import { EncString } from "@bitwarden/common/platform/models/domain/enc-string"; @@ -33,6 +34,7 @@ describe("DefaultSetPasswordJitService", () => { let apiService: MockProxy; let cryptoService: MockProxy; + let encryptService: MockProxy; let i18nService: MockProxy; let kdfConfigService: MockProxy; let masterPasswordService: MockProxy; @@ -43,6 +45,7 @@ describe("DefaultSetPasswordJitService", () => { beforeEach(() => { apiService = mock(); cryptoService = mock(); + encryptService = mock(); i18nService = mock(); kdfConfigService = mock(); masterPasswordService = mock(); @@ -53,6 +56,7 @@ describe("DefaultSetPasswordJitService", () => { sut = new DefaultSetPasswordJitService( apiService, cryptoService, + encryptService, i18nService, kdfConfigService, masterPasswordService, @@ -168,7 +172,7 @@ describe("DefaultSetPasswordJitService", () => { } cryptoService.userKey$.mockReturnValue(of(userKey)); - cryptoService.rsaEncrypt.mockResolvedValue(userKeyEncString); + encryptService.rsaEncrypt.mockResolvedValue(userKeyEncString); organizationUserApiService.putOrganizationUserResetPasswordEnrollment.mockResolvedValue( undefined, @@ -210,7 +214,7 @@ describe("DefaultSetPasswordJitService", () => { // Assert expect(apiService.setPassword).toHaveBeenCalledWith(setPasswordRequest); expect(organizationApiService.getKeys).toHaveBeenCalledWith(orgId); - expect(cryptoService.rsaEncrypt).toHaveBeenCalledWith(userKey.key, orgPublicKey); + expect(encryptService.rsaEncrypt).toHaveBeenCalledWith(userKey.key, orgPublicKey); expect( organizationUserApiService.putOrganizationUserResetPasswordEnrollment, ).toHaveBeenCalled(); diff --git a/libs/auth/src/angular/set-password-jit/default-set-password-jit.service.ts b/libs/auth/src/angular/set-password-jit/default-set-password-jit.service.ts index 968ba60dec3..1993877966f 100644 --- a/libs/auth/src/angular/set-password-jit/default-set-password-jit.service.ts +++ b/libs/auth/src/angular/set-password-jit/default-set-password-jit.service.ts @@ -14,6 +14,7 @@ import { PBKDF2KdfConfig } from "@bitwarden/common/auth/models/domain/kdf-config import { SetPasswordRequest } from "@bitwarden/common/auth/models/request/set-password.request"; import { KeysRequest } from "@bitwarden/common/models/request/keys.request"; import { CryptoService } from "@bitwarden/common/platform/abstractions/crypto.service"; +import { EncryptService } from "@bitwarden/common/platform/abstractions/encrypt.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { Utils } from "@bitwarden/common/platform/misc/utils"; import { EncString } from "@bitwarden/common/platform/models/domain/enc-string"; @@ -29,6 +30,7 @@ export class DefaultSetPasswordJitService implements SetPasswordJitService { constructor( protected apiService: ApiService, protected cryptoService: CryptoService, + protected encryptService: EncryptService, protected i18nService: I18nService, protected kdfConfigService: KdfConfigService, protected masterPasswordService: InternalMasterPasswordServiceAbstraction, @@ -157,7 +159,7 @@ export class DefaultSetPasswordJitService implements SetPasswordJitService { throw new Error("userKey not found. Could not handle reset password auto enroll."); } - const encryptedUserKey = await this.cryptoService.rsaEncrypt(userKey.key, publicKey); + const encryptedUserKey = await this.encryptService.rsaEncrypt(userKey.key, publicKey); const resetRequest = new OrganizationUserResetPasswordEnrollmentRequest(); resetRequest.masterPasswordHash = masterKeyHash; diff --git a/libs/auth/src/common/login-strategies/webauthn-login.strategy.spec.ts b/libs/auth/src/common/login-strategies/webauthn-login.strategy.spec.ts index b25022d25df..e4b1f740310 100644 --- a/libs/auth/src/common/login-strategies/webauthn-login.strategy.spec.ts +++ b/libs/auth/src/common/login-strategies/webauthn-login.strategy.spec.ts @@ -226,7 +226,7 @@ describe("WebAuthnLoginStrategy", () => { const mockUserKey = new SymmetricCryptoKey(mockUserKeyArray) as UserKey; encryptService.decryptToBytes.mockResolvedValue(mockPrfPrivateKey); - cryptoService.rsaDecrypt.mockResolvedValue(mockUserKeyArray); + encryptService.rsaDecrypt.mockResolvedValue(mockUserKeyArray); // Act await webAuthnLoginStrategy.logIn(webAuthnCredentials); @@ -244,9 +244,9 @@ describe("WebAuthnLoginStrategy", () => { idTokenResponse.userDecryptionOptions.webAuthnPrfOption.encryptedPrivateKey, webAuthnCredentials.prfKey, ); - expect(cryptoService.rsaDecrypt).toHaveBeenCalledTimes(1); - expect(cryptoService.rsaDecrypt).toHaveBeenCalledWith( - idTokenResponse.userDecryptionOptions.webAuthnPrfOption.encryptedUserKey.encryptedString, + expect(encryptService.rsaDecrypt).toHaveBeenCalledTimes(1); + expect(encryptService.rsaDecrypt).toHaveBeenCalledWith( + idTokenResponse.userDecryptionOptions.webAuthnPrfOption.encryptedUserKey, mockPrfPrivateKey, ); expect(cryptoService.setUserKey).toHaveBeenCalledWith(mockUserKey, userId); @@ -273,7 +273,7 @@ describe("WebAuthnLoginStrategy", () => { // Assert expect(encryptService.decryptToBytes).not.toHaveBeenCalled(); - expect(cryptoService.rsaDecrypt).not.toHaveBeenCalled(); + expect(encryptService.rsaDecrypt).not.toHaveBeenCalled(); expect(cryptoService.setUserKey).not.toHaveBeenCalled(); }); @@ -325,7 +325,7 @@ describe("WebAuthnLoginStrategy", () => { apiService.postIdentityToken.mockResolvedValue(idTokenResponse); - cryptoService.rsaDecrypt.mockResolvedValue(null); + encryptService.rsaDecrypt.mockResolvedValue(null); // Act await webAuthnLoginStrategy.logIn(webAuthnCredentials); diff --git a/libs/auth/src/common/login-strategies/webauthn-login.strategy.ts b/libs/auth/src/common/login-strategies/webauthn-login.strategy.ts index 96f8bc7d633..c5451d13df5 100644 --- a/libs/auth/src/common/login-strategies/webauthn-login.strategy.ts +++ b/libs/auth/src/common/login-strategies/webauthn-login.strategy.ts @@ -4,6 +4,7 @@ import { Jsonify } from "type-fest"; import { AuthResult } from "@bitwarden/common/auth/models/domain/auth-result"; import { WebAuthnLoginTokenRequest } from "@bitwarden/common/auth/models/request/identity-token/webauthn-login-token.request"; import { IdentityTokenResponse } from "@bitwarden/common/auth/models/response/identity-token.response"; +import { EncString } from "@bitwarden/common/platform/models/domain/enc-string"; import { SymmetricCryptoKey } from "@bitwarden/common/platform/models/domain/symmetric-crypto-key"; import { UserId } from "@bitwarden/common/types/guid"; import { UserKey } from "@bitwarden/common/types/key"; @@ -86,8 +87,8 @@ export class WebAuthnLoginStrategy extends LoginStrategy { ); // decrypt user key with private key - const userKey = await this.cryptoService.rsaDecrypt( - webAuthnPrfOption.encryptedUserKey.encryptedString, + const userKey = await this.encryptService.rsaDecrypt( + new EncString(webAuthnPrfOption.encryptedUserKey.encryptedString), privateKey, ); diff --git a/libs/auth/src/common/services/auth-request/auth-request.service.spec.ts b/libs/auth/src/common/services/auth-request/auth-request.service.spec.ts index 14f807a7708..58dbae6d789 100644 --- a/libs/auth/src/common/services/auth-request/auth-request.service.spec.ts +++ b/libs/auth/src/common/services/auth-request/auth-request.service.spec.ts @@ -6,6 +6,7 @@ import { FakeMasterPasswordService } from "@bitwarden/common/auth/services/maste import { AuthRequestPushNotification } from "@bitwarden/common/models/response/notification.response"; import { AppIdService } from "@bitwarden/common/platform/abstractions/app-id.service"; import { CryptoService } from "@bitwarden/common/platform/abstractions/crypto.service"; +import { EncryptService } from "@bitwarden/common/platform/abstractions/encrypt.service"; import { Utils } from "@bitwarden/common/platform/misc/utils"; import { EncString } from "@bitwarden/common/platform/models/domain/enc-string"; import { SymmetricCryptoKey } from "@bitwarden/common/platform/models/domain/symmetric-crypto-key"; @@ -24,6 +25,7 @@ describe("AuthRequestService", () => { let masterPasswordService: FakeMasterPasswordService; const appIdService = mock(); const cryptoService = mock(); + const encryptService = mock(); const apiService = mock(); let mockPrivateKey: Uint8Array; @@ -40,6 +42,7 @@ describe("AuthRequestService", () => { accountService, masterPasswordService, cryptoService, + encryptService, apiService, stateProvider, ); @@ -82,7 +85,7 @@ describe("AuthRequestService", () => { describe("approveOrDenyAuthRequest", () => { beforeEach(() => { - cryptoService.rsaEncrypt.mockResolvedValue({ + encryptService.rsaEncrypt.mockResolvedValue({ encryptedString: "ENCRYPTED_STRING", } as EncString); appIdService.getAppId.mockResolvedValue("APP_ID"); @@ -108,7 +111,7 @@ describe("AuthRequestService", () => { new AuthRequestResponse({ id: "123", publicKey: "KEY" }), ); - expect(cryptoService.rsaEncrypt).toHaveBeenCalledWith(new Uint8Array(64), expect.anything()); + expect(encryptService.rsaEncrypt).toHaveBeenCalledWith(new Uint8Array(64), expect.anything()); }); it("should use the user key if the master key and hash do not exist", async () => { @@ -119,7 +122,7 @@ describe("AuthRequestService", () => { new AuthRequestResponse({ id: "123", publicKey: "KEY" }), ); - expect(cryptoService.rsaEncrypt).toHaveBeenCalledWith(new Uint8Array(64), expect.anything()); + expect(encryptService.rsaEncrypt).toHaveBeenCalledWith(new Uint8Array(64), expect.anything()); }); }); describe("setUserKeyAfterDecryptingSharedUserKey", () => { @@ -211,7 +214,7 @@ describe("AuthRequestService", () => { const mockDecryptedUserKeyBytes = new Uint8Array(64); const mockDecryptedUserKey = new SymmetricCryptoKey(mockDecryptedUserKeyBytes) as UserKey; - cryptoService.rsaDecrypt.mockResolvedValueOnce(mockDecryptedUserKeyBytes); + encryptService.rsaDecrypt.mockResolvedValueOnce(mockDecryptedUserKeyBytes); // Act const result = await sut.decryptPubKeyEncryptedUserKey( @@ -220,7 +223,10 @@ describe("AuthRequestService", () => { ); // Assert - expect(cryptoService.rsaDecrypt).toBeCalledWith(mockPubKeyEncryptedUserKey, mockPrivateKey); + expect(encryptService.rsaDecrypt).toBeCalledWith( + new EncString(mockPubKeyEncryptedUserKey), + mockPrivateKey, + ); expect(result).toEqual(mockDecryptedUserKey); }); }); @@ -238,7 +244,7 @@ describe("AuthRequestService", () => { const mockDecryptedMasterKeyHashBytes = new Uint8Array(64); const mockDecryptedMasterKeyHash = Utils.fromBufferToUtf8(mockDecryptedMasterKeyHashBytes); - cryptoService.rsaDecrypt + encryptService.rsaDecrypt .mockResolvedValueOnce(mockDecryptedMasterKeyBytes) .mockResolvedValueOnce(mockDecryptedMasterKeyHashBytes); @@ -250,14 +256,14 @@ describe("AuthRequestService", () => { ); // Assert - expect(cryptoService.rsaDecrypt).toHaveBeenNthCalledWith( + expect(encryptService.rsaDecrypt).toHaveBeenNthCalledWith( 1, - mockPubKeyEncryptedMasterKey, + new EncString(mockPubKeyEncryptedMasterKey), mockPrivateKey, ); - expect(cryptoService.rsaDecrypt).toHaveBeenNthCalledWith( + expect(encryptService.rsaDecrypt).toHaveBeenNthCalledWith( 2, - mockPubKeyEncryptedMasterKeyHash, + new EncString(mockPubKeyEncryptedMasterKeyHash), mockPrivateKey, ); expect(result.masterKey).toEqual(mockDecryptedMasterKey); diff --git a/libs/auth/src/common/services/auth-request/auth-request.service.ts b/libs/auth/src/common/services/auth-request/auth-request.service.ts index eefee511f82..51926d65983 100644 --- a/libs/auth/src/common/services/auth-request/auth-request.service.ts +++ b/libs/auth/src/common/services/auth-request/auth-request.service.ts @@ -10,7 +10,9 @@ import { AuthRequestResponse } from "@bitwarden/common/auth/models/response/auth import { AuthRequestPushNotification } from "@bitwarden/common/models/response/notification.response"; import { AppIdService } from "@bitwarden/common/platform/abstractions/app-id.service"; import { CryptoService } from "@bitwarden/common/platform/abstractions/crypto.service"; +import { EncryptService } from "@bitwarden/common/platform/abstractions/encrypt.service"; import { Utils } from "@bitwarden/common/platform/misc/utils"; +import { EncString } from "@bitwarden/common/platform/models/domain/enc-string"; import { SymmetricCryptoKey } from "@bitwarden/common/platform/models/domain/symmetric-crypto-key"; import { AUTH_REQUEST_DISK_LOCAL, @@ -44,6 +46,7 @@ export class AuthRequestService implements AuthRequestServiceAbstraction { private accountService: AccountService, private masterPasswordService: InternalMasterPasswordServiceAbstraction, private cryptoService: CryptoService, + private encryptService: EncryptService, private apiService: ApiService, private stateProvider: StateProvider, ) { @@ -102,7 +105,7 @@ export class AuthRequestService implements AuthRequestServiceAbstraction { if (masterKey && masterKeyHash) { // Only encrypt the master password hash if masterKey exists as // we won't have a masterKeyHash without a masterKey - encryptedMasterKeyHash = await this.cryptoService.rsaEncrypt( + encryptedMasterKeyHash = await this.encryptService.rsaEncrypt( Utils.fromUtf8ToArray(masterKeyHash), pubKey, ); @@ -112,7 +115,7 @@ export class AuthRequestService implements AuthRequestServiceAbstraction { keyToEncrypt = userKey.key; } - const encryptedKey = await this.cryptoService.rsaEncrypt(keyToEncrypt, pubKey); + const encryptedKey = await this.encryptService.rsaEncrypt(keyToEncrypt, pubKey); const response = new PasswordlessAuthRequest( encryptedKey.encryptedString, @@ -161,8 +164,8 @@ export class AuthRequestService implements AuthRequestServiceAbstraction { pubKeyEncryptedUserKey: string, privateKey: Uint8Array, ): Promise { - const decryptedUserKeyBytes = await this.cryptoService.rsaDecrypt( - pubKeyEncryptedUserKey, + const decryptedUserKeyBytes = await this.encryptService.rsaDecrypt( + new EncString(pubKeyEncryptedUserKey), privateKey, ); @@ -174,13 +177,13 @@ export class AuthRequestService implements AuthRequestServiceAbstraction { pubKeyEncryptedMasterKeyHash: string, privateKey: Uint8Array, ): Promise<{ masterKey: MasterKey; masterKeyHash: string }> { - const decryptedMasterKeyArrayBuffer = await this.cryptoService.rsaDecrypt( - pubKeyEncryptedMasterKey, + const decryptedMasterKeyArrayBuffer = await this.encryptService.rsaDecrypt( + new EncString(pubKeyEncryptedMasterKey), privateKey, ); - const decryptedMasterKeyHashArrayBuffer = await this.cryptoService.rsaDecrypt( - pubKeyEncryptedMasterKeyHash, + const decryptedMasterKeyHashArrayBuffer = await this.encryptService.rsaDecrypt( + new EncString(pubKeyEncryptedMasterKeyHash), privateKey, ); diff --git a/libs/common/src/auth/services/device-trust.service.implementation.ts b/libs/common/src/auth/services/device-trust.service.implementation.ts index c1cf871e257..178f4b06545 100644 --- a/libs/common/src/auth/services/device-trust.service.implementation.ts +++ b/libs/common/src/auth/services/device-trust.service.implementation.ts @@ -144,7 +144,7 @@ export class DeviceTrustService implements DeviceTrustServiceAbstraction { deviceKeyEncryptedDevicePrivateKey, ] = await Promise.all([ // Encrypt user key with the DevicePublicKey - this.cryptoService.rsaEncrypt(userKey.key, devicePublicKey), + this.encryptService.rsaEncrypt(userKey.key, devicePublicKey), // Encrypt devicePublicKey with user key this.encryptService.encrypt(devicePublicKey, userKey), @@ -206,7 +206,7 @@ export class DeviceTrustService implements DeviceTrustServiceAbstraction { ); // Encrypt the brand new user key with the now-decrypted public key for the device - const encryptedNewUserKey = await this.cryptoService.rsaEncrypt( + const encryptedNewUserKey = await this.encryptService.rsaEncrypt( newUserKey.key, decryptedDevicePublicKey, ); @@ -317,8 +317,8 @@ export class DeviceTrustService implements DeviceTrustServiceAbstraction { ); // Attempt to decrypt encryptedUserDataKey with devicePrivateKey - const userKey = await this.cryptoService.rsaDecrypt( - encryptedUserKey.encryptedString, + const userKey = await this.encryptService.rsaDecrypt( + new EncString(encryptedUserKey.encryptedString), devicePrivateKey, ); diff --git a/libs/common/src/auth/services/device-trust.service.spec.ts b/libs/common/src/auth/services/device-trust.service.spec.ts index 7afd38ec0ad..1171ae2918a 100644 --- a/libs/common/src/auth/services/device-trust.service.spec.ts +++ b/libs/common/src/auth/services/device-trust.service.spec.ts @@ -372,7 +372,7 @@ describe("deviceTrustService", () => { .mockResolvedValue(mockUserKey); cryptoSvcRsaEncryptSpy = jest - .spyOn(cryptoService, "rsaEncrypt") + .spyOn(encryptService, "rsaEncrypt") .mockResolvedValue(mockDevicePublicKeyEncryptedUserKey); encryptServiceEncryptSpy = jest @@ -577,7 +577,7 @@ describe("deviceTrustService", () => { .spyOn(encryptService, "decryptToBytes") .mockResolvedValue(new Uint8Array(userKeyBytesLength)); const rsaDecryptSpy = jest - .spyOn(cryptoService, "rsaDecrypt") + .spyOn(encryptService, "rsaDecrypt") .mockResolvedValue(new Uint8Array(userKeyBytesLength)); const result = await deviceTrustService.decryptUserKeyWithDeviceKey( @@ -696,7 +696,7 @@ describe("deviceTrustService", () => { }); // Mock the encryption of the new user key with the decrypted public key - cryptoService.rsaEncrypt.mockImplementationOnce((data, publicKey) => { + encryptService.rsaEncrypt.mockImplementationOnce((data, publicKey) => { expect(data.byteLength).toBe(64); // New key should also be 64 bytes expect(new Uint8Array(data)[0]).toBe(FakeNewUserKeyMarker); // New key should have the first byte be '1'; diff --git a/libs/common/src/auth/services/password-reset-enrollment.service.implementation.spec.ts b/libs/common/src/auth/services/password-reset-enrollment.service.implementation.spec.ts index 575b3a6ee79..b78ef52f07f 100644 --- a/libs/common/src/auth/services/password-reset-enrollment.service.implementation.spec.ts +++ b/libs/common/src/auth/services/password-reset-enrollment.service.implementation.spec.ts @@ -2,6 +2,7 @@ import { mock, MockProxy } from "jest-mock-extended"; import { BehaviorSubject } from "rxjs"; import { OrganizationUserApiService } from "@bitwarden/admin-console/common"; +import { EncryptService } from "@bitwarden/common/platform/abstractions/encrypt.service"; import { UserId } from "../../../../common/src/types/guid"; import { OrganizationApiServiceAbstraction } from "../../admin-console/abstractions/organization/organization-api.service.abstraction"; @@ -18,6 +19,7 @@ describe("PasswordResetEnrollmentServiceImplementation", () => { let organizationApiService: MockProxy; let accountService: MockProxy; let cryptoService: MockProxy; + let encryptService: MockProxy; let organizationUserApiService: MockProxy; let i18nService: MockProxy; let service: PasswordResetEnrollmentServiceImplementation; @@ -27,12 +29,14 @@ describe("PasswordResetEnrollmentServiceImplementation", () => { accountService = mock(); accountService.activeAccount$ = activeAccountSubject; cryptoService = mock(); + encryptService = mock(); organizationUserApiService = mock(); i18nService = mock(); service = new PasswordResetEnrollmentServiceImplementation( organizationApiService, accountService, cryptoService, + encryptService, organizationUserApiService, i18nService, ); @@ -96,7 +100,7 @@ describe("PasswordResetEnrollmentServiceImplementation", () => { activeAccountSubject.next(Object.assign(user1AccountInfo, { id: "userId" as UserId })); cryptoService.getUserKey.mockResolvedValue({ key: "key" } as any); - cryptoService.rsaEncrypt.mockResolvedValue(encryptedKey as any); + encryptService.rsaEncrypt.mockResolvedValue(encryptedKey as any); await service.enroll("orgId"); @@ -118,7 +122,7 @@ describe("PasswordResetEnrollmentServiceImplementation", () => { }; const encryptedKey = { encryptedString: "encryptedString" }; organizationApiService.getKeys.mockResolvedValue(orgKeyResponse as any); - cryptoService.rsaEncrypt.mockResolvedValue(encryptedKey as any); + encryptService.rsaEncrypt.mockResolvedValue(encryptedKey as any); await service.enroll("orgId", "userId", { key: "key" } as any); diff --git a/libs/common/src/auth/services/password-reset-enrollment.service.implementation.ts b/libs/common/src/auth/services/password-reset-enrollment.service.implementation.ts index 65718d96694..7dc5f449959 100644 --- a/libs/common/src/auth/services/password-reset-enrollment.service.implementation.ts +++ b/libs/common/src/auth/services/password-reset-enrollment.service.implementation.ts @@ -4,6 +4,7 @@ import { OrganizationUserApiService, OrganizationUserResetPasswordEnrollmentRequest, } from "@bitwarden/admin-console/common"; +import { EncryptService } from "@bitwarden/common/platform/abstractions/encrypt.service"; import { OrganizationApiServiceAbstraction } from "../../admin-console/abstractions/organization/organization-api.service.abstraction"; import { CryptoService } from "../../platform/abstractions/crypto.service"; @@ -20,6 +21,7 @@ export class PasswordResetEnrollmentServiceImplementation protected organizationApiService: OrganizationApiServiceAbstraction, protected accountService: AccountService, protected cryptoService: CryptoService, + protected encryptService: EncryptService, protected organizationUserApiService: OrganizationUserApiService, protected i18nService: I18nService, ) {} @@ -47,7 +49,7 @@ export class PasswordResetEnrollmentServiceImplementation userId ?? (await firstValueFrom(this.accountService.activeAccount$.pipe(map((a) => a?.id)))); userKey = userKey ?? (await this.cryptoService.getUserKey(userId)); // RSA Encrypt user's userKey.key with organization public key - const encryptedKey = await this.cryptoService.rsaEncrypt(userKey.key, orgPublicKey); + const encryptedKey = await this.encryptService.rsaEncrypt(userKey.key, orgPublicKey); const resetRequest = new OrganizationUserResetPasswordEnrollmentRequest(); resetRequest.resetPasswordKey = encryptedKey.encryptedString; diff --git a/libs/common/src/autofill/utils.ts b/libs/common/src/autofill/utils.ts index 86411691ea2..0b127e4b9da 100644 --- a/libs/common/src/autofill/utils.ts +++ b/libs/common/src/autofill/utils.ts @@ -82,10 +82,10 @@ export function isCardExpired(cipherCard: CardView): boolean { const parsedYear = parseInt(normalizedYear, 10); - // First day of the next month minus one, to get last day of the card month - const cardExpiry = new Date(parsedYear, parsedMonth + 1, 0); + // First day of the next month + const cardExpiry = new Date(parsedYear, parsedMonth + 1, 1); - return cardExpiry < now; + return cardExpiry <= now; } } diff --git a/libs/common/src/enums/feature-flag.enum.ts b/libs/common/src/enums/feature-flag.enum.ts index 505fe33e82a..3be73466cb5 100644 --- a/libs/common/src/enums/feature-flag.enum.ts +++ b/libs/common/src/enums/feature-flag.enum.ts @@ -9,7 +9,6 @@ export enum FeatureFlag { GeneratorToolsModernization = "generator-tools-modernization", EnableConsolidatedBilling = "enable-consolidated-billing", AC1795_UpdatedSubscriptionStatusSection = "AC-1795_updated-subscription-status-section", - EnableDeleteProvider = "AC-1218-delete-provider", ExtensionRefresh = "extension-refresh", PersistPopupView = "persist-popup-view", PM4154_BulkEncryptionService = "PM-4154-bulk-encryption-service", @@ -21,7 +20,6 @@ export enum FeatureFlag { EnableTimeThreshold = "PM-5864-dollar-threshold", InlineMenuPositioningImprovements = "inline-menu-positioning-improvements", ProviderClientVaultPrivacyBanner = "ac-2833-provider-client-vault-privacy-banner", - VaultBulkManagementAction = "vault-bulk-management-action", AC2828_ProviderPortalMembersPage = "AC-2828_provider-portal-members-page", IdpAutoSubmitLogin = "idp-auto-submit-login", UnauthenticatedExtensionUIRefresh = "unauth-ui-refresh", @@ -34,6 +32,7 @@ export enum FeatureFlag { AC2476_DeprecateStripeSourcesAPI = "AC-2476-deprecate-stripe-sources-api", CipherKeyEncryption = "cipher-key-encryption", PM11901_RefactorSelfHostingLicenseUploader = "PM-11901-refactor-self-hosting-license-uploader", + Pm3478RefactorOrganizationUserApi = "pm-3478-refactor-organizationuser-api", } export type AllowedFeatureFlagTypes = boolean | number | string; @@ -53,7 +52,6 @@ export const DefaultFeatureFlagValue = { [FeatureFlag.GeneratorToolsModernization]: FALSE, [FeatureFlag.EnableConsolidatedBilling]: FALSE, [FeatureFlag.AC1795_UpdatedSubscriptionStatusSection]: FALSE, - [FeatureFlag.EnableDeleteProvider]: FALSE, [FeatureFlag.ExtensionRefresh]: FALSE, [FeatureFlag.PersistPopupView]: FALSE, [FeatureFlag.PM4154_BulkEncryptionService]: FALSE, @@ -65,7 +63,6 @@ export const DefaultFeatureFlagValue = { [FeatureFlag.EnableTimeThreshold]: FALSE, [FeatureFlag.InlineMenuPositioningImprovements]: FALSE, [FeatureFlag.ProviderClientVaultPrivacyBanner]: FALSE, - [FeatureFlag.VaultBulkManagementAction]: FALSE, [FeatureFlag.AC2828_ProviderPortalMembersPage]: FALSE, [FeatureFlag.IdpAutoSubmitLogin]: FALSE, [FeatureFlag.UnauthenticatedExtensionUIRefresh]: FALSE, @@ -78,6 +75,7 @@ export const DefaultFeatureFlagValue = { [FeatureFlag.AC2476_DeprecateStripeSourcesAPI]: FALSE, [FeatureFlag.CipherKeyEncryption]: FALSE, [FeatureFlag.PM11901_RefactorSelfHostingLicenseUploader]: FALSE, + [FeatureFlag.Pm3478RefactorOrganizationUserApi]: FALSE, } satisfies Record; export type DefaultFeatureFlagValueType = typeof DefaultFeatureFlagValue; diff --git a/libs/common/src/platform/abstractions/crypto.service.ts b/libs/common/src/platform/abstractions/crypto.service.ts index 2a8e1ad6476..020cfb81754 100644 --- a/libs/common/src/platform/abstractions/crypto.service.ts +++ b/libs/common/src/platform/abstractions/crypto.service.ts @@ -329,22 +329,6 @@ export abstract class CryptoService { * @param userId The user's Id */ abstract clearKeys(userId?: string): Promise; - /** - * RSA encrypts a value. - * @param data The data to encrypt - * @param publicKey The public key to use for encryption, if not provided, the user's public key will be used - * @returns The encrypted data - * @throws If the given publicKey is a null-ish value. - */ - abstract rsaEncrypt(data: Uint8Array, publicKey: Uint8Array): Promise; - /** - * Decrypts a value using RSA. - * @param encValue The encrypted value to decrypt - * @param privateKey The private key to use for decryption - * @returns The decrypted value - * @throws If the given privateKey is a null-ish value. - */ - abstract rsaDecrypt(encValue: string, privateKey: Uint8Array): Promise; abstract randomNumber(min: number, max: number): Promise; /** * Generates a new cipher key diff --git a/libs/common/src/platform/models/domain/domain-base.spec.ts b/libs/common/src/platform/models/domain/domain-base.spec.ts new file mode 100644 index 00000000000..0bdee21e3c1 --- /dev/null +++ b/libs/common/src/platform/models/domain/domain-base.spec.ts @@ -0,0 +1,139 @@ +import { mock, MockProxy } from "jest-mock-extended"; + +import { makeEncString, makeSymmetricCryptoKey } from "../../../../spec"; +import { EncryptService } from "../../abstractions/encrypt.service"; +import { Utils } from "../../misc/utils"; + +import Domain from "./domain-base"; +import { EncString } from "./enc-string"; + +class TestDomain extends Domain { + plainText: string; + encToString: EncString; + encString2: EncString; +} + +describe("DomainBase", () => { + let encryptService: MockProxy; + const key = makeSymmetricCryptoKey(64); + + beforeEach(() => { + encryptService = mock(); + }); + + function setUpCryptography() { + encryptService.encrypt.mockImplementation((value) => { + let data: string; + if (typeof value === "string") { + data = value; + } else { + data = Utils.fromBufferToUtf8(value); + } + + return Promise.resolve(makeEncString(data)); + }); + + encryptService.decryptToUtf8.mockImplementation((value) => { + return Promise.resolve(value.data); + }); + + encryptService.decryptToBytes.mockImplementation((value) => { + return Promise.resolve(value.dataBytes); + }); + } + + describe("decryptWithKey", () => { + it("domain property types are decryptable", async () => { + const domain = new TestDomain(); + + await domain["decryptObjWithKey"]( + // @ts-expect-error -- clear is not of type EncString + ["plainText"], + makeSymmetricCryptoKey(64), + mock(), + ); + + await domain["decryptObjWithKey"]( + // @ts-expect-error -- Clear is not of type EncString + ["encToString", "encString2", "plainText"], + makeSymmetricCryptoKey(64), + mock(), + ); + + const decrypted = await domain["decryptObjWithKey"]( + ["encToString"], + makeSymmetricCryptoKey(64), + mock(), + ); + + // @ts-expect-error -- encString2 was not decrypted + decrypted as { encToString: string; encString2: string; plainText: string }; + + // encString2 was not decrypted, so it's still an EncString + decrypted as { encToString: string; encString2: EncString; plainText: string }; + }); + + it("decrypts the encrypted properties", async () => { + setUpCryptography(); + + const domain = new TestDomain(); + + domain.encToString = await encryptService.encrypt("string", key); + + const decrypted = await domain["decryptObjWithKey"](["encToString"], key, encryptService); + + expect(decrypted).toEqual({ + encToString: "string", + }); + }); + + it("decrypts multiple encrypted properties", async () => { + setUpCryptography(); + + const domain = new TestDomain(); + + domain.encToString = await encryptService.encrypt("string", key); + domain.encString2 = await encryptService.encrypt("string2", key); + + const decrypted = await domain["decryptObjWithKey"]( + ["encToString", "encString2"], + key, + encryptService, + ); + + expect(decrypted).toEqual({ + encToString: "string", + encString2: "string2", + }); + }); + + it("does not decrypt properties that are not encrypted", async () => { + const domain = new TestDomain(); + domain.plainText = "clear"; + + const decrypted = await domain["decryptObjWithKey"]([], key, encryptService); + + expect(decrypted).toEqual({ + plainText: "clear", + }); + }); + + it("does not decrypt properties that were not requested to be decrypted", async () => { + setUpCryptography(); + + const domain = new TestDomain(); + + domain.plainText = "clear"; + domain.encToString = makeEncString("string"); + domain.encString2 = makeEncString("string2"); + + const decrypted = await domain["decryptObjWithKey"]([], key, encryptService); + + expect(decrypted).toEqual({ + plainText: "clear", + encToString: makeEncString("string"), + encString2: makeEncString("string2"), + }); + }); + }); +}); diff --git a/libs/common/src/platform/models/domain/domain-base.ts b/libs/common/src/platform/models/domain/domain-base.ts index f7273d2435b..1cfcfac02ff 100644 --- a/libs/common/src/platform/models/domain/domain-base.ts +++ b/libs/common/src/platform/models/domain/domain-base.ts @@ -1,8 +1,18 @@ +import { ConditionalExcept, ConditionalKeys, Constructor } from "type-fest"; + import { View } from "../../../models/view/view"; +import { EncryptService } from "../../abstractions/encrypt.service"; import { EncString } from "./enc-string"; import { SymmetricCryptoKey } from "./symmetric-crypto-key"; +// eslint-disable-next-line @typescript-eslint/ban-types +type EncStringKeys = ConditionalKeys, EncString>; +export type DecryptedObject< + TEncryptedObject, + TDecryptedKeys extends EncStringKeys, +> = Record & Omit; + // https://contributing.bitwarden.com/architecture/clients/data-model#domain export default class Domain { protected buildDomainModel( @@ -80,4 +90,60 @@ export default class Domain { await Promise.all(promises); return viewModel; } + + /** + * Decrypts the requested properties of the domain object with the provided key and encrypt service. + * + * If a property is null, the result will be null. + * @see {@link EncString.decryptWithKey} for more details on decryption behavior. + * + * @param encryptedProperties The properties to decrypt. Type restricted to EncString properties of the domain object. + * @param key The key to use for decryption. + * @param encryptService The encryption service to use for decryption. + * @param _ The constructor of the domain object. Used for type inference if the domain object is not automatically inferred. + * @returns An object with the requested properties decrypted and the rest of the domain object untouched. + */ + protected async decryptObjWithKey< + TThis extends Domain, + const TEncryptedKeys extends EncStringKeys, + >( + this: TThis, + encryptedProperties: TEncryptedKeys[], + key: SymmetricCryptoKey, + encryptService: EncryptService, + _: Constructor = this.constructor as Constructor, + ): Promise> { + const promises = []; + + for (const prop of encryptedProperties) { + const value = (this as any)[prop] as EncString; + promises.push(this.decryptProperty(prop, value, key, encryptService)); + } + + const decryptedObjects = await Promise.all(promises); + const decryptedObject = decryptedObjects.reduce( + (acc, obj) => { + return { ...acc, ...obj }; + }, + { ...this }, + ); + return decryptedObject as DecryptedObject; + } + + private async decryptProperty>( + propertyKey: TEncryptedKeys, + value: EncString, + key: SymmetricCryptoKey, + encryptService: EncryptService, + ) { + let decrypted: string = null; + if (value) { + decrypted = await value.decryptWithKey(key, encryptService); + } else { + decrypted = null; + } + return { + [propertyKey]: decrypted, + }; + } } diff --git a/libs/common/src/platform/models/domain/enc-string.spec.ts b/libs/common/src/platform/models/domain/enc-string.spec.ts index 7583c37e1e7..39d58831772 100644 --- a/libs/common/src/platform/models/domain/enc-string.spec.ts +++ b/libs/common/src/platform/models/domain/enc-string.spec.ts @@ -1,11 +1,12 @@ import { mock, MockProxy } from "jest-mock-extended"; -import { makeStaticByteArray } from "../../../../spec"; +import { makeEncString, makeStaticByteArray } from "../../../../spec"; import { EncryptService } from "../../../platform/abstractions/encrypt.service"; import { SymmetricCryptoKey } from "../../../platform/models/domain/symmetric-crypto-key"; import { UserKey, OrgKey } from "../../../types/key"; import { CryptoService } from "../../abstractions/crypto.service"; import { EncryptionType } from "../../enums"; +import { Utils } from "../../misc/utils"; import { ContainerService } from "../../services/container.service"; import { EncString } from "./enc-string"; @@ -113,6 +114,77 @@ describe("EncString", () => { }); }); + describe("decryptWithKey", () => { + const encString = new EncString(EncryptionType.Rsa2048_OaepSha256_B64, "data"); + + const cryptoService = mock(); + const encryptService = mock(); + encryptService.decryptToUtf8 + .calledWith(encString, expect.anything()) + .mockResolvedValue("decrypted"); + + function setupEncryption() { + encryptService.encrypt.mockImplementation(async (data, key) => { + if (typeof data === "string") { + return makeEncString(data); + } else { + return makeEncString(Utils.fromBufferToUtf8(data)); + } + }); + encryptService.decryptToUtf8.mockImplementation(async (encString, key) => { + return encString.data; + }); + encryptService.decryptToBytes.mockImplementation(async (encString, key) => { + return encString.dataBytes; + }); + } + + beforeEach(() => { + (window as any).bitwardenContainerService = new ContainerService( + cryptoService, + encryptService, + ); + }); + + it("decrypts using the provided key and encryptService", async () => { + setupEncryption(); + + const key = new SymmetricCryptoKey(makeStaticByteArray(32)); + await encString.decryptWithKey(key, encryptService); + + expect(encryptService.decryptToUtf8).toHaveBeenCalledWith(encString, key); + }); + + it("fails to decrypt when key is null", async () => { + const decrypted = await encString.decryptWithKey(null, encryptService); + + expect(decrypted).toBe("[error: cannot decrypt]"); + expect(encString.decryptedValue).toBe("[error: cannot decrypt]"); + }); + + it("fails to decrypt when encryptService is null", async () => { + const decrypted = await encString.decryptWithKey( + new SymmetricCryptoKey(makeStaticByteArray(32)), + null, + ); + + expect(decrypted).toBe("[error: cannot decrypt]"); + expect(encString.decryptedValue).toBe("[error: cannot decrypt]"); + }); + + it("fails to decrypt when encryptService throws", async () => { + encryptService.decryptToUtf8.mockRejectedValue("error"); + + const decrypted = await encString.decryptWithKey( + new SymmetricCryptoKey(makeStaticByteArray(32)), + encryptService, + ); + + expect(decrypted).toBe("[error: cannot decrypt]"); + expect(encString.decryptedValue).toBe("[error: cannot decrypt]"); + }); + }); + describe("AesCbc256_B64", () => { it("constructor", () => { const encString = new EncString(EncryptionType.AesCbc256_B64, "data", "iv"); diff --git a/libs/common/src/platform/models/domain/enc-string.ts b/libs/common/src/platform/models/domain/enc-string.ts index 1f4c8caf39c..0b0a597acd3 100644 --- a/libs/common/src/platform/models/domain/enc-string.ts +++ b/libs/common/src/platform/models/domain/enc-string.ts @@ -1,11 +1,14 @@ import { Jsonify, Opaque } from "type-fest"; +import { EncryptService } from "../../abstractions/encrypt.service"; import { EncryptionType, EXPECTED_NUM_PARTS_BY_ENCRYPTION_TYPE } from "../../enums"; import { Encrypted } from "../../interfaces/encrypted"; import { Utils } from "../../misc/utils"; import { SymmetricCryptoKey } from "./symmetric-crypto-key"; +export const DECRYPT_ERROR = "[error: cannot decrypt]"; + export class EncString implements Encrypted { encryptedString?: EncryptedString; encryptionType?: EncryptionType; @@ -167,11 +170,24 @@ export class EncString implements Encrypted { const encryptService = Utils.getContainerService().getEncryptService(); this.decryptedValue = await encryptService.decryptToUtf8(this, key); } catch (e) { - this.decryptedValue = "[error: cannot decrypt]"; + this.decryptedValue = DECRYPT_ERROR; } return this.decryptedValue; } + async decryptWithKey(key: SymmetricCryptoKey, encryptService: EncryptService) { + try { + if (key == null) { + throw new Error("No key to decrypt EncString"); + } + + this.decryptedValue = await encryptService.decryptToUtf8(this, key); + } catch (e) { + this.decryptedValue = DECRYPT_ERROR; + } + + return this.decryptedValue; + } private async getKeyForDecryption(orgId: string) { const cryptoService = Utils.getContainerService().getCryptoService(); return orgId != null diff --git a/libs/common/src/platform/services/crypto.service.ts b/libs/common/src/platform/services/crypto.service.ts index 6a93ac7f3ff..6b2afdb9806 100644 --- a/libs/common/src/platform/services/crypto.service.ts +++ b/libs/common/src/platform/services/crypto.service.ts @@ -45,7 +45,7 @@ import { KeyGenerationService } from "../abstractions/key-generation.service"; import { LogService } from "../abstractions/log.service"; import { PlatformUtilsService } from "../abstractions/platform-utils.service"; import { StateService } from "../abstractions/state.service"; -import { KeySuffixOptions, HashPurpose, EncryptionType } from "../enums"; +import { KeySuffixOptions, HashPurpose } from "../enums"; import { convertValues } from "../misc/convert-values"; import { EFFLongWordList } from "../misc/wordlist"; import { EncString, EncryptedString } from "../models/domain/enc-string"; @@ -441,7 +441,7 @@ export class CryptoService implements CryptoServiceAbstraction { const shareKey = await this.keyGenerationService.createKey(512); userId ??= await firstValueFrom(this.stateProvider.activeUserId$); const publicKey = await firstValueFrom(this.userPublicKey$(userId)); - const encShareKey = await this.rsaEncrypt(shareKey.key, publicKey); + const encShareKey = await this.encryptService.rsaEncrypt(shareKey.key, publicKey); return [encShareKey, shareKey as T]; } @@ -550,68 +550,6 @@ export class CryptoService implements CryptoServiceAbstraction { await this.stateProvider.setUserState(USER_EVER_HAD_USER_KEY, null, userId); } - async rsaEncrypt(data: Uint8Array, publicKey: Uint8Array): Promise { - if (publicKey == null) { - throw new Error("'publicKey' is a required parameter and must be non-null"); - } - - const encBytes = await this.cryptoFunctionService.rsaEncrypt(data, publicKey, "sha1"); - return new EncString(EncryptionType.Rsa2048_OaepSha1_B64, Utils.fromBufferToB64(encBytes)); - } - - async rsaDecrypt(encValue: string, privateKey: Uint8Array): Promise { - if (privateKey == null) { - throw new Error("'privateKey' is a required parameter and must be non-null"); - } - - const headerPieces = encValue.split("."); - let encType: EncryptionType = null; - let encPieces: string[]; - - if (headerPieces.length === 1) { - encType = EncryptionType.Rsa2048_OaepSha256_B64; - encPieces = [headerPieces[0]]; - } else if (headerPieces.length === 2) { - try { - encType = parseInt(headerPieces[0], null); - encPieces = headerPieces[1].split("|"); - } catch (e) { - this.logService.error(e); - } - } - - switch (encType) { - case EncryptionType.Rsa2048_OaepSha256_B64: - case EncryptionType.Rsa2048_OaepSha1_B64: - case EncryptionType.Rsa2048_OaepSha256_HmacSha256_B64: // HmacSha256 types are deprecated - case EncryptionType.Rsa2048_OaepSha1_HmacSha256_B64: - break; - default: - throw new Error("encType unavailable."); - } - - if (encPieces == null || encPieces.length <= 0) { - throw new Error("encPieces unavailable."); - } - - const data = Utils.fromB64ToArray(encPieces[0]); - - let alg: "sha1" | "sha256" = "sha1"; - switch (encType) { - case EncryptionType.Rsa2048_OaepSha256_B64: - case EncryptionType.Rsa2048_OaepSha256_HmacSha256_B64: - alg = "sha256"; - break; - case EncryptionType.Rsa2048_OaepSha1_B64: - case EncryptionType.Rsa2048_OaepSha1_HmacSha256_B64: - break; - default: - throw new Error("encType unavailable."); - } - - return this.cryptoFunctionService.rsaDecrypt(data, privateKey, alg); - } - // EFForg/OpenWireless // ref https://github.com/EFForg/OpenWireless/blob/master/app/js/diceware.js async randomNumber(min: number, max: number): Promise { diff --git a/libs/common/src/platform/services/system.service.ts b/libs/common/src/platform/services/system.service.ts index 382b3bf8e86..357737391c2 100644 --- a/libs/common/src/platform/services/system.service.ts +++ b/libs/common/src/platform/services/system.service.ts @@ -1,5 +1,7 @@ import { firstValueFrom, map, Subscription, timeout } from "rxjs"; +import { BiometricStateService } from "@bitwarden/key-management"; + import { PinServiceAbstraction } from "../../../../auth/src/common/abstractions"; import { VaultTimeoutSettingsService } from "../../abstractions/vault-timeout/vault-timeout-settings.service"; import { AccountService } from "../../auth/abstractions/account.service"; @@ -11,7 +13,6 @@ import { UserId } from "../../types/guid"; import { MessagingService } from "../abstractions/messaging.service"; import { PlatformUtilsService } from "../abstractions/platform-utils.service"; import { SystemService as SystemServiceAbstraction } from "../abstractions/system.service"; -import { BiometricStateService } from "../biometrics/biometric-state.service"; import { Utils } from "../misc/utils"; import { ScheduledTaskNames } from "../scheduling/scheduled-task-name.enum"; import { TaskSchedulerService } from "../scheduling/task-scheduler.service"; diff --git a/libs/common/src/services/vault-timeout/vault-timeout-settings.service.spec.ts b/libs/common/src/services/vault-timeout/vault-timeout-settings.service.spec.ts index 177c75ed5b8..d90388f866f 100644 --- a/libs/common/src/services/vault-timeout/vault-timeout-settings.service.spec.ts +++ b/libs/common/src/services/vault-timeout/vault-timeout-settings.service.spec.ts @@ -8,6 +8,7 @@ import { } from "@bitwarden/auth/common"; import { Utils } from "@bitwarden/common/platform/misc/utils"; import { UserId } from "@bitwarden/common/types/guid"; +import { BiometricStateService } from "@bitwarden/key-management"; import { FakeAccountService, mockAccountServiceWith, FakeStateProvider } from "../../../spec"; import { VaultTimeoutSettingsService as VaultTimeoutSettingsServiceAbstraction } from "../../abstractions/vault-timeout/vault-timeout-settings.service"; @@ -17,7 +18,6 @@ import { TokenService } from "../../auth/abstractions/token.service"; import { VaultTimeoutAction } from "../../enums/vault-timeout-action.enum"; import { CryptoService } from "../../platform/abstractions/crypto.service"; import { LogService } from "../../platform/abstractions/log.service"; -import { BiometricStateService } from "../../platform/biometrics/biometric-state.service"; import { VAULT_TIMEOUT, VAULT_TIMEOUT_ACTION, diff --git a/libs/common/src/services/vault-timeout/vault-timeout-settings.service.ts b/libs/common/src/services/vault-timeout/vault-timeout-settings.service.ts index e6587ade70d..a90842b208c 100644 --- a/libs/common/src/services/vault-timeout/vault-timeout-settings.service.ts +++ b/libs/common/src/services/vault-timeout/vault-timeout-settings.service.ts @@ -17,6 +17,7 @@ import { PinServiceAbstraction, UserDecryptionOptionsServiceAbstraction, } from "@bitwarden/auth/common"; +import { BiometricStateService } from "@bitwarden/key-management"; import { VaultTimeoutSettingsService as VaultTimeoutSettingsServiceAbstraction } from "../../abstractions/vault-timeout/vault-timeout-settings.service"; import { PolicyService } from "../../admin-console/abstractions/policy/policy.service.abstraction"; @@ -27,7 +28,6 @@ import { TokenService } from "../../auth/abstractions/token.service"; import { VaultTimeoutAction } from "../../enums/vault-timeout-action.enum"; import { CryptoService } from "../../platform/abstractions/crypto.service"; import { LogService } from "../../platform/abstractions/log.service"; -import { BiometricStateService } from "../../platform/biometrics/biometric-state.service"; import { StateProvider } from "../../platform/state"; import { UserId } from "../../types/guid"; import { VaultTimeout, VaultTimeoutStringType } from "../../types/vault-timeout.type"; diff --git a/libs/common/src/tools/send/services/send-api.service.abstraction.ts b/libs/common/src/tools/send/services/send-api.service.abstraction.ts index 100985c4870..4109df19680 100644 --- a/libs/common/src/tools/send/services/send-api.service.abstraction.ts +++ b/libs/common/src/tools/send/services/send-api.service.abstraction.ts @@ -36,5 +36,5 @@ export abstract class SendApiService { renewSendFileUploadUrl: (sendId: string, fileId: string) => Promise; removePassword: (id: string) => Promise; delete: (id: string) => Promise; - save: (sendData: [Send, EncArrayBuffer]) => Promise; + save: (sendData: [Send, EncArrayBuffer]) => Promise; } diff --git a/libs/common/src/tools/send/services/send-api.service.ts b/libs/common/src/tools/send/services/send-api.service.ts index 2cb2ff1c2f0..ff71408bce3 100644 --- a/libs/common/src/tools/send/services/send-api.service.ts +++ b/libs/common/src/tools/send/services/send-api.service.ts @@ -135,11 +135,12 @@ export class SendApiService implements SendApiServiceAbstraction { return this.apiService.send("DELETE", "/sends/" + id, null, true, false); } - async save(sendData: [Send, EncArrayBuffer]): Promise { + async save(sendData: [Send, EncArrayBuffer]): Promise { const response = await this.upload(sendData); const data = new SendData(response); await this.sendService.upsert(data); + return new Send(data); } async delete(id: string): Promise { diff --git a/libs/common/src/vault/models/domain/folder.spec.ts b/libs/common/src/vault/models/domain/folder.spec.ts index 69134d19cfb..785852b884e 100644 --- a/libs/common/src/vault/models/domain/folder.spec.ts +++ b/libs/common/src/vault/models/domain/folder.spec.ts @@ -1,4 +1,7 @@ -import { mockEnc, mockFromJson } from "../../../../spec"; +import { mock, MockProxy } from "jest-mock-extended"; + +import { makeEncString, makeSymmetricCryptoKey, mockEnc, mockFromJson } from "../../../../spec"; +import { EncryptService } from "../../../platform/abstractions/encrypt.service"; import { EncryptedString, EncString } from "../../../platform/models/domain/enc-string"; import { FolderData } from "../../models/data/folder.data"; import { Folder } from "../../models/domain/folder"; @@ -60,4 +63,42 @@ describe("Folder", () => { expect(actual).toMatchObject(expected); }); }); + + describe("decryptWithKey", () => { + let encryptService: MockProxy; + const key = makeSymmetricCryptoKey(64); + + beforeEach(() => { + encryptService = mock(); + encryptService.decryptToUtf8.mockImplementation((value) => { + return Promise.resolve(value.data); + }); + }); + + it("decrypts the name", async () => { + const folder = new Folder(); + folder.name = makeEncString("encName"); + + const view = await folder.decryptWithKey(key, encryptService); + + expect(view).toEqual({ + name: "encName", + }); + }); + + it("assigns the folder id and revision date", async () => { + const folder = new Folder(); + folder.id = "id"; + folder.revisionDate = new Date("2022-01-31T12:00:00.000Z"); + + const view = await folder.decryptWithKey(key, encryptService); + + expect(view).toEqual( + expect.objectContaining({ + id: "id", + revisionDate: new Date("2022-01-31T12:00:00.000Z"), + }), + ); + }); + }); }); diff --git a/libs/common/src/vault/models/domain/folder.ts b/libs/common/src/vault/models/domain/folder.ts index 9505bad7058..da9e9811d4c 100644 --- a/libs/common/src/vault/models/domain/folder.ts +++ b/libs/common/src/vault/models/domain/folder.ts @@ -1,10 +1,18 @@ import { Jsonify } from "type-fest"; +import { EncryptService } from "../../../platform/abstractions/encrypt.service"; import Domain from "../../../platform/models/domain/domain-base"; import { EncString } from "../../../platform/models/domain/enc-string"; +import { SymmetricCryptoKey } from "../../../platform/models/domain/symmetric-crypto-key"; import { FolderData } from "../data/folder.data"; import { FolderView } from "../view/folder.view"; +export class Test extends Domain { + id: string; + name: EncString; + revisionDate: Date; +} + export class Folder extends Domain { id: string; name: EncString; @@ -39,6 +47,17 @@ export class Folder extends Domain { ); } + async decryptWithKey( + key: SymmetricCryptoKey, + encryptService: EncryptService, + ): Promise { + const decrypted = await this.decryptObjWithKey(["name"], key, encryptService, Folder); + + const view = new FolderView(decrypted); + view.name = decrypted.name; + return view; + } + static fromJSON(obj: Jsonify) { const revisionDate = obj.revisionDate == null ? null : new Date(obj.revisionDate); return Object.assign(new Folder(), obj, { name: EncString.fromJSON(obj.name), revisionDate }); diff --git a/libs/common/src/vault/models/view/folder.view.ts b/libs/common/src/vault/models/view/folder.view.ts index 7e5c51bc307..47659c2739c 100644 --- a/libs/common/src/vault/models/view/folder.view.ts +++ b/libs/common/src/vault/models/view/folder.view.ts @@ -1,6 +1,7 @@ import { Jsonify } from "type-fest"; import { View } from "../../../models/view/view"; +import { DecryptedObject } from "../../../platform/models/domain/domain-base"; import { Folder } from "../domain/folder"; import { ITreeNodeObject } from "../domain/tree-node"; @@ -9,7 +10,7 @@ export class FolderView implements View, ITreeNodeObject { name: string = null; revisionDate: Date = null; - constructor(f?: Folder) { + constructor(f?: Folder | DecryptedObject) { if (!f) { return; } diff --git a/libs/components/src/item/item-content.component.html b/libs/components/src/item/item-content.component.html index bed8b2f5b76..d8e7b31c830 100644 --- a/libs/components/src/item/item-content.component.html +++ b/libs/components/src/item/item-content.component.html @@ -2,8 +2,16 @@
-
- +
+
+ +
+
+ +
diff --git a/libs/components/src/item/item.mdx b/libs/components/src/item/item.mdx index 507d7d56a22..b5c7da80baa 100644 --- a/libs/components/src/item/item.mdx +++ b/libs/components/src/item/item.mdx @@ -55,12 +55,13 @@ The content can be a button, anchor, or static container. `bit-item-content` contains the following slots to help position the content: -| Slot | Description | -| ------------------ | --------------------------------------------------- | -| default | primary text or arbitrary content; fan favorite | -| `slot="secondary"` | supporting text; under the default slot | -| `slot="start"` | commonly an icon or avatar; before the default slot | -| `slot="end"` | commonly an icon; after the default slot | +| Slot | Description | +| ------------------------- | --------------------------------------------------------------------------------------------------------- | +| default | primary text or arbitrary content; fan favorite | +| `slot="secondary"` | supporting text; under the default slot | +| `slot="start"` | commonly an icon or avatar; before the default slot | +| `slot="default-trailing"` | commonly a badge; default content that should not be truncated and is placed right after the default slot | +| `slot="end"` | commonly an icon; placed at the far end after the default slot | - Note: There is also an `end` slot within `bit-item` itself. Place [interactive secondary actions](#secondary-actions) there, and place non-interactive content (such @@ -71,6 +72,7 @@ The content can be a button, anchor, or static container. diff --git a/libs/tools/send/send-ui/src/new-send-dropdown/new-send-dropdown.component.ts b/libs/tools/send/send-ui/src/new-send-dropdown/new-send-dropdown.component.ts index 620dc77c995..7dbe184d981 100644 --- a/libs/tools/send/send-ui/src/new-send-dropdown/new-send-dropdown.component.ts +++ b/libs/tools/send/send-ui/src/new-send-dropdown/new-send-dropdown.component.ts @@ -1,5 +1,5 @@ import { CommonModule } from "@angular/common"; -import { Component, OnInit } from "@angular/core"; +import { Component, Input, OnInit } from "@angular/core"; import { Router, RouterLink } from "@angular/router"; import { firstValueFrom } from "rxjs"; @@ -15,6 +15,8 @@ import { BadgeModule, ButtonModule, MenuModule } from "@bitwarden/components"; imports: [JslibModule, CommonModule, ButtonModule, RouterLink, MenuModule, BadgeModule], }) export class NewSendDropdownComponent implements OnInit { + @Input() hideIcon: boolean = false; + sendType = SendType; hasNoPremium = false; diff --git a/libs/tools/send/send-ui/src/send-form/components/options/send-options.component.html b/libs/tools/send/send-ui/src/send-form/components/options/send-options.component.html index 5c03a8c431d..adbca181947 100644 --- a/libs/tools/send/send-ui/src/send-form/components/options/send-options.component.html +++ b/libs/tools/send/send-ui/src/send-form/components/options/send-options.component.html @@ -30,6 +30,16 @@ (click)="generatePassword()" data-testid="generate-password" > + {{ "sendPasswordDescV2" | i18n }} diff --git a/libs/tools/send/send-ui/src/send-form/components/send-details/send-text-details.component.html b/libs/tools/send/send-ui/src/send-form/components/send-details/send-text-details.component.html index b7875d1d45f..e24b96e21ee 100644 --- a/libs/tools/send/send-ui/src/send-form/components/send-details/send-text-details.component.html +++ b/libs/tools/send/send-ui/src/send-form/components/send-details/send-text-details.component.html @@ -1,7 +1,7 @@ {{ "sendTypeTextToShare" | i18n }} - + diff --git a/libs/tools/send/send-ui/src/send-form/components/send-form.component.ts b/libs/tools/send/send-ui/src/send-form/components/send-form.component.ts index 1d93804e11f..07939ccb06c 100644 --- a/libs/tools/send/send-ui/src/send-form/components/send-form.component.ts +++ b/libs/tools/send/send-ui/src/send-form/components/send-form.component.ts @@ -85,9 +85,14 @@ export class SendFormComponent implements AfterViewInit, OnInit, OnChanges, Send submitBtn?: ButtonComponent; /** - * Event emitted when the send is saved successfully. + * Event emitted when the send is created successfully. */ - @Output() sendSaved = new EventEmitter(); + @Output() onSendCreated = new EventEmitter(); + + /** + * Event emitted when the send is updated successfully. + */ + @Output() onSendUpdated = new EventEmitter(); /** * The original send being edited or cloned. Null for add mode. @@ -200,22 +205,26 @@ export class SendFormComponent implements AfterViewInit, OnInit, OnChanges, Send return; } + const sendView = await this.addEditFormService.saveSend( + this.updatedSendView, + this.file, + this.config, + ); + + if (this.config.mode === "add") { + this.onSendCreated.emit(sendView); + return; + } + if (Utils.isNullOrWhitespace(this.updatedSendView.password)) { this.updatedSendView.password = null; } - await this.addEditFormService.saveSend(this.updatedSendView, this.file, this.config); - this.toastService.showToast({ variant: "success", title: null, - message: this.i18nService.t( - this.config.mode === "edit" || this.config.mode === "partial-edit" - ? "editedItem" - : "addedItem", - ), + message: this.i18nService.t("editedItem"), }); - - this.sendSaved.emit(this.updatedSendView); + this.onSendUpdated.emit(this.updatedSendView); }; } diff --git a/libs/tools/send/send-ui/src/send-form/services/default-send-form.service.ts b/libs/tools/send/send-ui/src/send-form/services/default-send-form.service.ts index 9b6a6360ac7..9eb37b07e50 100644 --- a/libs/tools/send/send-ui/src/send-form/services/default-send-form.service.ts +++ b/libs/tools/send/send-ui/src/send-form/services/default-send-form.service.ts @@ -19,6 +19,7 @@ export class DefaultSendFormService implements SendFormService { async saveSend(send: SendView, file: File | ArrayBuffer, config: SendFormConfig) { const sendData = await this.sendService.encrypt(send, file, send.password, null); - return await this.sendApiService.save(sendData); + const newSend = await this.sendApiService.save(sendData); + return await this.decryptSend(newSend); } } diff --git a/libs/tools/send/send-ui/src/services/send-items.service.ts b/libs/tools/send/send-ui/src/services/send-items.service.ts index 66ad5b67864..6cef663f891 100644 --- a/libs/tools/send/send-ui/src/services/send-items.service.ts +++ b/libs/tools/send/send-ui/src/services/send-items.service.ts @@ -83,7 +83,7 @@ export class SendItemsService { ); /** - * Observable that indicates whether the user's vault is empty. + * Observable that indicates whether the user's send list is empty. */ emptyList$: Observable = this._sendList$.pipe(map((sends) => !sends.length)); diff --git a/libs/vault/src/cipher-form/components/attachments/cipher-attachments.component.html b/libs/vault/src/cipher-form/components/attachments/cipher-attachments.component.html index f7414bd8d3c..ca083769dd9 100644 --- a/libs/vault/src/cipher-form/components/attachments/cipher-attachments.component.html +++ b/libs/vault/src/cipher-form/components/attachments/cipher-attachments.component.html @@ -1,6 +1,6 @@

{{ "attachments" | i18n }}

-
    +
    • diff --git a/libs/vault/src/cipher-form/components/attachments/delete-attachment/delete-attachment.component.html b/libs/vault/src/cipher-form/components/attachments/delete-attachment/delete-attachment.component.html index 38ece650b72..efbc1a0503c 100644 --- a/libs/vault/src/cipher-form/components/attachments/delete-attachment/delete-attachment.component.html +++ b/libs/vault/src/cipher-form/components/attachments/delete-attachment/delete-attachment.component.html @@ -3,7 +3,7 @@ buttonType="danger" size="small" type="button" - class="tw-border-none" + class="tw-border-transparent" [appA11yTitle]="'deleteAttachmentName' | i18n: attachment.fileName" [bitAction]="delete" > diff --git a/package-lock.json b/package-lock.json index 39cfb49a545..3a4c9e91842 100644 --- a/package-lock.json +++ b/package-lock.json @@ -24,6 +24,7 @@ "@angular/platform-browser": "16.2.12", "@angular/platform-browser-dynamic": "16.2.12", "@angular/router": "16.2.12", + "@electron/fuses": "1.8.0", "@koa/multer": "3.0.2", "@koa/router": "12.0.1", "@microsoft/signalr": "8.0.7", @@ -43,7 +44,7 @@ "https-proxy-agent": "7.0.5", "inquirer": "8.2.6", "jquery": "3.7.1", - "jsdom": "24.1.3", + "jsdom": "25.0.1", "jszip": "3.10.1", "koa": "2.15.0", "koa-bodyparser": "4.4.1", @@ -66,7 +67,7 @@ "qrious": "4.0.2", "rxjs": "7.8.1", "tabbable": "6.2.0", - "tldts": "6.1.46", + "tldts": "6.1.48", "utf-8-validate": "6.0.4", "zone.js": "0.13.3", "zxcvbn": "4.4.2" @@ -82,7 +83,7 @@ "@babel/core": "7.24.9", "@babel/preset-env": "7.24.8", "@compodoc/compodoc": "1.1.25", - "@electron/notarize": "2.4.0", + "@electron/notarize": "2.5.0", "@electron/rebuild": "3.6.0", "@ngtools/webpack": "16.2.14", "@storybook/addon-a11y": "8.2.9", @@ -96,7 +97,7 @@ "@storybook/theming": "8.2.9", "@types/argon2-browser": "1.18.4", "@types/chrome": "0.0.272", - "@types/firefox-webext-browser": "111.0.5", + "@types/firefox-webext-browser": "120.0.4", "@types/inquirer": "8.2.10", "@types/jest": "29.5.12", "@types/jquery": "3.5.30", @@ -108,7 +109,7 @@ "@types/koa-json": "2.0.23", "@types/lowdb": "1.0.15", "@types/lunr": "2.3.7", - "@types/node": "20.16.5", + "@types/node": "20.16.10", "@types/node-fetch": "2.6.4", "@types/node-forge": "1.3.11", "@types/node-ipc": "9.2.3", @@ -162,7 +163,7 @@ "postcss": "8.4.38", "postcss-loader": "8.1.1", "prettier": "3.3.3", - "prettier-plugin-tailwindcss": "0.6.6", + "prettier-plugin-tailwindcss": "0.6.8", "process": "0.11.10", "regedit": "3.0.3", "remark-gfm": "4.0.0", @@ -209,7 +210,7 @@ "form-data": "4.0.0", "https-proxy-agent": "7.0.5", "inquirer": "8.2.6", - "jsdom": "24.1.3", + "jsdom": "25.0.1", "jszip": "3.10.1", "koa": "2.15.0", "koa-bodyparser": "4.4.1", @@ -223,7 +224,7 @@ "papaparse": "5.4.1", "proper-lockfile": "4.1.2", "rxjs": "7.8.1", - "tldts": "6.1.46", + "tldts": "6.1.48", "zxcvbn": "4.4.2" }, "bin": { @@ -291,6 +292,16 @@ "@bitwarden/vault-export-core": "file:../tools/export/vault-export/vault-export-core" } }, + "libs/key-management": { + "name": "@bitwarden/key-management", + "version": "0.0.0", + "license": "GPL-3.0", + "dependencies": { + "@bitwarden/angular": "file:../angular", + "@bitwarden/common": "file:../common", + "@bitwarden/components": "file:../components" + } + }, "libs/node": { "name": "@bitwarden/node", "version": "0.0.0", @@ -358,14 +369,14 @@ } }, "node_modules/@angular-devkit/architect": { - "version": "0.1802.5", - "resolved": "https://registry.npmjs.org/@angular-devkit/architect/-/architect-0.1802.5.tgz", - "integrity": "sha512-c7sVoW85Yqj7IYvNKxtNSGS5I7gWpORorg/xxLZX3OkHWXDrwYbb5LN/2p5/Aytxyb0aXl4o5fFOu6CUwcaLUw==", + "version": "0.1802.6", + "resolved": "https://registry.npmjs.org/@angular-devkit/architect/-/architect-0.1802.6.tgz", + "integrity": "sha512-oF7cPFdTLxeuvXkK/opSdIxZ1E4LrBbmuytQ/nCoAGOaKBWdqvwagRZ6jVhaI0Gwu48rkcV7Zhesg/ESNnROdw==", "dev": true, "license": "MIT", "peer": true, "dependencies": { - "@angular-devkit/core": "18.2.5", + "@angular-devkit/core": "18.2.6", "rxjs": "7.8.1" }, "engines": { @@ -1439,9 +1450,9 @@ } }, "node_modules/@angular-devkit/core": { - "version": "18.2.5", - "resolved": "https://registry.npmjs.org/@angular-devkit/core/-/core-18.2.5.tgz", - "integrity": "sha512-r9TumPlJ8PvA2+yz4sp+bUHgtznaVKzhvXTN5qL1k4YP8LJ7iZWMR2FOP+HjukHZOTsenzmV9pszbogabqwoZQ==", + "version": "18.2.6", + "resolved": "https://registry.npmjs.org/@angular-devkit/core/-/core-18.2.6.tgz", + "integrity": "sha512-la4CFvs5PcRWSkQ/H7TB5cPZirFVA9GoWk5LzIk8si6VjWBJRm8b3keKJoC9LlNeABRUIR5z0ocYkyQQUhdMfg==", "dev": true, "license": "MIT", "peer": true, @@ -4602,6 +4613,10 @@ "resolved": "libs/importer", "link": true }, + "node_modules/@bitwarden/key-management": { + "resolved": "libs/key-management", + "link": true + }, "node_modules/@bitwarden/node": { "resolved": "libs/node", "link": true @@ -5045,6 +5060,17 @@ "node": ">=10.12.0" } }, + "node_modules/@electron/asar/node_modules/@types/glob": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/@types/glob/-/glob-7.1.3.tgz", + "integrity": "sha512-SEYeGAIQIQX8NN6LDKprLjbrd5dARM5EXsd8GI/A5l0apYI1fGMWgPHSe4ZKL4eozlAyI+doUE9XbYS4xCkQ1w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/minimatch": "*", + "@types/node": "*" + } + }, "node_modules/@electron/asar/node_modules/brace-expansion": { "version": "1.1.11", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", @@ -5101,6 +5127,33 @@ "node": "*" } }, + "node_modules/@electron/fuses": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/@electron/fuses/-/fuses-1.8.0.tgz", + "integrity": "sha512-zx0EIq78WlY/lBb1uXlziZmDZI4ubcCXIMJ4uGjXzZW0nS19TjSPeXPAjzzTmKQlJUZm0SbmZhPKP7tuQ1SsEw==", + "dependencies": { + "chalk": "^4.1.1", + "fs-extra": "^9.0.1", + "minimist": "^1.2.5" + }, + "bin": { + "electron-fuses": "dist/bin.js" + } + }, + "node_modules/@electron/fuses/node_modules/fs-extra": { + "version": "9.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-9.1.0.tgz", + "integrity": "sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ==", + "dependencies": { + "at-least-node": "^1.0.0", + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/@electron/get": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/@electron/get/-/get-2.0.3.tgz", @@ -5169,9 +5222,9 @@ } }, "node_modules/@electron/notarize": { - "version": "2.4.0", - "resolved": "https://registry.npmjs.org/@electron/notarize/-/notarize-2.4.0.tgz", - "integrity": "sha512-ArHnRPIJJGrmV+uWNQSINAht+cM4gAo3uA3WFI54bYF93mzmD15gzhPQ0Dd+v/fkMhnRiiIO8NNkGdn87Vsy0g==", + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/@electron/notarize/-/notarize-2.5.0.tgz", + "integrity": "sha512-jNT8nwH1f9X5GEITXaQ8IF/KdskvIkOFfB2CvwumsveVidzpSc+mvhhTMdAGSYF3O+Nq49lJ7y+ssODRXu06+A==", "dev": true, "license": "MIT", "dependencies": { @@ -5375,6 +5428,57 @@ "node": "*" } }, + "node_modules/@esbuild/android-arm": { + "version": "0.18.17", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.18.17.tgz", + "integrity": "sha512-wHsmJG/dnL3OkpAcwbgoBTTMHVi4Uyou3F5mf58ZtmUyIKfcdA7TROav/6tCzET4A3QW2Q2FC+eFneMU+iyOxg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.18.17", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.18.17.tgz", + "integrity": "sha512-9np+YYdNDed5+Jgr1TdWBsozZ85U1Oa3xW0c7TWqH0y2aGghXtZsuT8nYRbzOMcl0bXZXjOGbksoTtVOlWrRZg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.18.17", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.18.17.tgz", + "integrity": "sha512-O+FeWB/+xya0aLg23hHEM2E3hbfwZzjqumKMSIqcHbNvDa+dza2D0yLuymRBQQnC34CWrsJUXyH2MG5VnLd6uw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, "node_modules/@esbuild/darwin-arm64": { "version": "0.18.17", "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.18.17.tgz", @@ -5392,6 +5496,312 @@ "node": ">=12" } }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.18.17", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.18.17.tgz", + "integrity": "sha512-XDre+J5YeIJDMfp3n0279DFNrGCXlxOuGsWIkRb1NThMZ0BsrWXoTg23Jer7fEXQ9Ye5QjrvXpxnhzl3bHtk0g==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.18.17", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.18.17.tgz", + "integrity": "sha512-cjTzGa3QlNfERa0+ptykyxs5A6FEUQQF0MuilYXYBGdBxD3vxJcKnzDlhDCa1VAJCmAxed6mYhA2KaJIbtiNuQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.18.17", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.18.17.tgz", + "integrity": "sha512-sOxEvR8d7V7Kw8QqzxWc7bFfnWnGdaFBut1dRUYtu+EIRXefBc/eIsiUiShnW0hM3FmQ5Zf27suDuHsKgZ5QrA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.18.17", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.18.17.tgz", + "integrity": "sha512-2d3Lw6wkwgSLC2fIvXKoMNGVaeY8qdN0IC3rfuVxJp89CRfA3e3VqWifGDfuakPmp90+ZirmTfye1n4ncjv2lg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.18.17", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.18.17.tgz", + "integrity": "sha512-c9w3tE7qA3CYWjT+M3BMbwMt+0JYOp3vCMKgVBrCl1nwjAlOMYzEo+gG7QaZ9AtqZFj5MbUc885wuBBmu6aADQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.18.17", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.18.17.tgz", + "integrity": "sha512-1DS9F966pn5pPnqXYz16dQqWIB0dmDfAQZd6jSSpiT9eX1NzKh07J6VKR3AoXXXEk6CqZMojiVDSZi1SlmKVdg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.18.17", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.18.17.tgz", + "integrity": "sha512-EvLsxCk6ZF0fpCB6w6eOI2Fc8KW5N6sHlIovNe8uOFObL2O+Mr0bflPHyHwLT6rwMg9r77WOAWb2FqCQrVnwFg==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.18.17", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.18.17.tgz", + "integrity": "sha512-e0bIdHA5p6l+lwqTE36NAW5hHtw2tNRmHlGBygZC14QObsA3bD4C6sXLJjvnDIjSKhW1/0S3eDy+QmX/uZWEYQ==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.18.17", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.18.17.tgz", + "integrity": "sha512-BAAilJ0M5O2uMxHYGjFKn4nJKF6fNCdP1E0o5t5fvMYYzeIqy2JdAP88Az5LHt9qBoUa4tDaRpfWt21ep5/WqQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.18.17", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.18.17.tgz", + "integrity": "sha512-Wh/HW2MPnC3b8BqRSIme/9Zhab36PPH+3zam5pqGRH4pE+4xTrVLx2+XdGp6fVS3L2x+DrsIcsbMleex8fbE6g==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.18.17", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.18.17.tgz", + "integrity": "sha512-j/34jAl3ul3PNcK3pfI0NSlBANduT2UO5kZ7FCaK33XFv3chDhICLY8wJJWIhiQ+YNdQ9dxqQctRg2bvrMlYgg==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.18.17", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.18.17.tgz", + "integrity": "sha512-QM50vJ/y+8I60qEmFxMoxIx4de03pGo2HwxdBeFd4nMh364X6TIBZ6VQ5UQmPbQWUVWHWws5MmJXlHAXvJEmpQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.18.17", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.18.17.tgz", + "integrity": "sha512-/jGlhWR7Sj9JPZHzXyyMZ1RFMkNPjC6QIAan0sDOtIo2TYk3tZn5UDrkE0XgsTQCxWTTOcMPf9p6Rh2hXtl5TQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.18.17", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.18.17.tgz", + "integrity": "sha512-rSEeYaGgyGGf4qZM2NonMhMOP/5EHp4u9ehFiBrg7stH6BYEEjlkVREuDEcQ0LfIl53OXLxNbfuIj7mr5m29TA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.18.17", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.18.17.tgz", + "integrity": "sha512-Y7ZBbkLqlSgn4+zot4KUNYst0bFoO68tRgI6mY2FIM+b7ZbyNVtNbDP5y8qlu4/knZZ73fgJDlXID+ohY5zt5g==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.18.17", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.18.17.tgz", + "integrity": "sha512-bwPmTJsEQcbZk26oYpc4c/8PvTY3J5/QK8jM19DVlEsAB41M39aWovWoHtNm78sd6ip6prilxeHosPADXtEJFw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.18.17", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.18.17.tgz", + "integrity": "sha512-H/XaPtPKli2MhW+3CQueo6Ni3Avggi6hP/YvgkEe1aSaxw+AeO8MFjq8DlgfTd9Iz4Yih3QCZI6YLMoyccnPRg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.18.17", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.18.17.tgz", + "integrity": "sha512-fGEb8f2BSA3CW7riJVurug65ACLuQAzKq0SSqkY2b2yHHH0MzDfbLyKIGzHwOI/gkHcxM/leuSW6D5w/LMNitA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, "node_modules/@eslint-community/eslint-utils": { "version": "4.4.0", "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.4.0.tgz", @@ -7726,9 +8136,9 @@ } }, "node_modules/@storybook/angular/node_modules/@storybook/components": { - "version": "8.3.3", - "resolved": "https://registry.npmjs.org/@storybook/components/-/components-8.3.3.tgz", - "integrity": "sha512-i2JYtesFGkdu+Hwuj+o9fLuO3yo+LPT1/8o5xBVYtEqsgDtEAyuRUWjSz8d8NPtzloGPOv5kvR6MokWDfbeMfw==", + "version": "8.3.4", + "resolved": "https://registry.npmjs.org/@storybook/components/-/components-8.3.4.tgz", + "integrity": "sha512-iQzLJd87uGbFBbYNqlrN/ABrnx3dUrL0tjPCarzglzshZoPCNOsllJeJx5TJwB9kCxSZ8zB9TTOgr7NXl+oyVA==", "dev": true, "license": "MIT", "funding": { @@ -7736,13 +8146,13 @@ "url": "https://opencollective.com/storybook" }, "peerDependencies": { - "storybook": "^8.3.3" + "storybook": "^8.3.4" } }, "node_modules/@storybook/angular/node_modules/@storybook/preview-api": { - "version": "8.3.3", - "resolved": "https://registry.npmjs.org/@storybook/preview-api/-/preview-api-8.3.3.tgz", - "integrity": "sha512-GP2QlaF3BBQGAyo248N7549YkTQjCentsc1hUvqPnFWU4xfjkejbnFk8yLaIw0VbYbL7jfd7npBtjZ+6AnphMQ==", + "version": "8.3.4", + "resolved": "https://registry.npmjs.org/@storybook/preview-api/-/preview-api-8.3.4.tgz", + "integrity": "sha512-/YKQ3QDVSHmtFXXCShf5w0XMlg8wkfTpdYxdGv1CKFV8DU24f3N7KWulAgeWWCWQwBzZClDa9kzxmroKlQqx3A==", "dev": true, "license": "MIT", "funding": { @@ -7750,13 +8160,13 @@ "url": "https://opencollective.com/storybook" }, "peerDependencies": { - "storybook": "^8.3.3" + "storybook": "^8.3.4" } }, "node_modules/@storybook/angular/node_modules/@types/node": { - "version": "18.19.50", - "resolved": "https://registry.npmjs.org/@types/node/-/node-18.19.50.tgz", - "integrity": "sha512-xonK+NRrMBRtkL1hVCc3G+uXtjh1Al4opBLjqVmipe5ZAaBYWW6cNAiBVZ1BvmkBhep698rP3UM3aRAdSALuhg==", + "version": "18.19.54", + "resolved": "https://registry.npmjs.org/@types/node/-/node-18.19.54.tgz", + "integrity": "sha512-+BRgt0G5gYjTvdLac9sIeE0iZcJxi4Jc4PV5EUzqi+88jmQLr+fRZdv2tCTV7IHKSGxM6SaLoOXQWWUiLUItMw==", "dev": true, "license": "MIT", "dependencies": { @@ -7859,9 +8269,9 @@ } }, "node_modules/@storybook/builder-webpack5/node_modules/@types/node": { - "version": "18.19.50", - "resolved": "https://registry.npmjs.org/@types/node/-/node-18.19.50.tgz", - "integrity": "sha512-xonK+NRrMBRtkL1hVCc3G+uXtjh1Al4opBLjqVmipe5ZAaBYWW6cNAiBVZ1BvmkBhep698rP3UM3aRAdSALuhg==", + "version": "18.19.54", + "resolved": "https://registry.npmjs.org/@types/node/-/node-18.19.54.tgz", + "integrity": "sha512-+BRgt0G5gYjTvdLac9sIeE0iZcJxi4Jc4PV5EUzqi+88jmQLr+fRZdv2tCTV7IHKSGxM6SaLoOXQWWUiLUItMw==", "dev": true, "license": "MIT", "dependencies": { @@ -8054,9 +8464,9 @@ } }, "node_modules/@storybook/core-webpack/node_modules/@types/node": { - "version": "18.19.50", - "resolved": "https://registry.npmjs.org/@types/node/-/node-18.19.50.tgz", - "integrity": "sha512-xonK+NRrMBRtkL1hVCc3G+uXtjh1Al4opBLjqVmipe5ZAaBYWW6cNAiBVZ1BvmkBhep698rP3UM3aRAdSALuhg==", + "version": "18.19.54", + "resolved": "https://registry.npmjs.org/@types/node/-/node-18.19.54.tgz", + "integrity": "sha512-+BRgt0G5gYjTvdLac9sIeE0iZcJxi4Jc4PV5EUzqi+88jmQLr+fRZdv2tCTV7IHKSGxM6SaLoOXQWWUiLUItMw==", "dev": true, "license": "MIT", "dependencies": { @@ -8071,9 +8481,9 @@ "license": "MIT" }, "node_modules/@storybook/core/node_modules/@types/node": { - "version": "18.19.50", - "resolved": "https://registry.npmjs.org/@types/node/-/node-18.19.50.tgz", - "integrity": "sha512-xonK+NRrMBRtkL1hVCc3G+uXtjh1Al4opBLjqVmipe5ZAaBYWW6cNAiBVZ1BvmkBhep698rP3UM3aRAdSALuhg==", + "version": "18.19.54", + "resolved": "https://registry.npmjs.org/@types/node/-/node-18.19.54.tgz", + "integrity": "sha512-+BRgt0G5gYjTvdLac9sIeE0iZcJxi4Jc4PV5EUzqi+88jmQLr+fRZdv2tCTV7IHKSGxM6SaLoOXQWWUiLUItMw==", "dev": true, "license": "MIT", "dependencies": { @@ -8674,9 +9084,22 @@ } }, "node_modules/@types/express-serve-static-core": { - "version": "4.19.5", - "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-4.19.5.tgz", - "integrity": "sha512-y6W03tvrACO72aijJ5uF02FRq5cgDR9lUxddQ8vyF+GvmjJQqbzDcJngEjURc+ZsG31VI3hODNZJ2URj86pzmg==", + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-5.0.0.tgz", + "integrity": "sha512-AbXMTZGt40T+KON9/Fdxx0B2WK5hsgxcfXJLr5bFpZ7b4JCex2WyQPTEKdXqfHiY5nKKBScZ7yCoO6Pvgxfvnw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*", + "@types/qs": "*", + "@types/range-parser": "*", + "@types/send": "*" + } + }, + "node_modules/@types/express/node_modules/@types/express-serve-static-core": { + "version": "4.19.6", + "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-4.19.6.tgz", + "integrity": "sha512-N4LZ2xG7DatVqhCZzOGb1Yi5lMbXSZcmdLDe9EzSndPV2HpWYWzRbaerl2n27irrm94EPpprqa8KpskPT085+A==", "dev": true, "license": "MIT", "dependencies": { @@ -8704,9 +9127,9 @@ "license": "MIT" }, "node_modules/@types/firefox-webext-browser": { - "version": "111.0.5", - "resolved": "https://registry.npmjs.org/@types/firefox-webext-browser/-/firefox-webext-browser-111.0.5.tgz", - "integrity": "sha512-YYE+4MeJvq7DZ+UzPD8c5uN1HJpGu4Fl6O6PEAfBJQmLzQkfTWlgMjZMJQHAmcH3rjVS5fjN+jMkkZ4ZTlKbmA==", + "version": "120.0.4", + "resolved": "https://registry.npmjs.org/@types/firefox-webext-browser/-/firefox-webext-browser-120.0.4.tgz", + "integrity": "sha512-lBrpf08xhiZBigrtdQfUaqX1UauwZ+skbFiL8u2Tdra/rklkKadYmIzTwkNZSWtuZ7OKpFqbE2HHfDoFqvZf6w==", "dev": true, "license": "MIT" }, @@ -8720,24 +9143,6 @@ "@types/node": "*" } }, - "node_modules/@types/glob": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/@types/glob/-/glob-7.2.0.tgz", - "integrity": "sha512-ZUxbzKl0IfJILTS6t7ip5fQQM/J3TJYubDm3nMbgubNNYS62eXeUpoLUC8/7fJNiFYHTrGPQn7hspDUzIHX3UA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/minimatch": "*", - "@types/node": "*" - } - }, - "node_modules/@types/glob/node_modules/@types/minimatch": { - "version": "3.0.5", - "resolved": "https://registry.npmjs.org/@types/minimatch/-/minimatch-3.0.5.tgz", - "integrity": "sha512-Klz949h02Gz2uZCMGwDUSDS1YBlTdDDgbWHi+81l29tQALUtvz4rAYi5uoVhE5Lagoq6DeqAUlbrHvW/mXDgdQ==", - "dev": true, - "license": "MIT" - }, "node_modules/@types/graceful-fs": { "version": "4.1.9", "resolved": "https://registry.npmjs.org/@types/graceful-fs/-/graceful-fs-4.1.9.tgz", @@ -8750,9 +9155,9 @@ } }, "node_modules/@types/har-format": { - "version": "1.2.15", - "resolved": "https://registry.npmjs.org/@types/har-format/-/har-format-1.2.15.tgz", - "integrity": "sha512-RpQH4rXLuvTXKR0zqHq3go0RVXYv/YVqv4TnPH95VbwUxZdQlK1EtcMvQvMpDngHbt13Csh9Z4qT9AbkiQH5BA==", + "version": "1.2.16", + "resolved": "https://registry.npmjs.org/@types/har-format/-/har-format-1.2.16.tgz", + "integrity": "sha512-fluxdy7ryD3MV6h8pTfTYpy/xQzCFC7m89nOH9y94cNqJ1mDIDPut7MnRHI3F6qRmh/cT2fUjG1MLdCNb4hE9A==", "dev": true, "license": "MIT" }, @@ -9056,6 +9461,13 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/minimatch": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/@types/minimatch/-/minimatch-3.0.5.tgz", + "integrity": "sha512-Klz949h02Gz2uZCMGwDUSDS1YBlTdDDgbWHi+81l29tQALUtvz4rAYi5uoVhE5Lagoq6DeqAUlbrHvW/mXDgdQ==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/ms": { "version": "0.7.34", "resolved": "https://registry.npmjs.org/@types/ms/-/ms-0.7.34.tgz", @@ -9064,9 +9476,9 @@ "license": "MIT" }, "node_modules/@types/node": { - "version": "20.16.5", - "resolved": "https://registry.npmjs.org/@types/node/-/node-20.16.5.tgz", - "integrity": "sha512-VwYCweNo3ERajwy0IUlqqcyZ8/A7Zwa9ZP3MnENWcB11AejO+tLy3pu850goUW2FC/IJMdZUfKpX/yxL1gymCA==", + "version": "20.16.10", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.16.10.tgz", + "integrity": "sha512-vQUKgWTjEIRFCvK6CyriPH3MZYiYlNy0fKiEYHWbcoWLEgs4opurGGKlebrTLqdSMIbXImH6XExNiIyNUv3WpA==", "dev": true, "license": "MIT", "dependencies": { @@ -9180,9 +9592,9 @@ "license": "MIT" }, "node_modules/@types/react": { - "version": "18.3.9", - "resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.9.tgz", - "integrity": "sha512-+BpAVyTpJkNWWSSnaLBk6ePpHLOGJKnEQNbINNovPWzvEUyAe3e+/d494QdEh71RekM/qV7lw6jzf1HGrJyAtQ==", + "version": "18.3.10", + "resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.10.tgz", + "integrity": "sha512-02sAAlBnP39JgXwkAq3PeU9DVaaGpZyF3MGcC0MKgQVkZor5IiiDAipVaxQHtDJAmO4GIy/rVBy/LzVj76Cyqg==", "dev": true, "license": "MIT", "dependencies": { @@ -11984,9 +12396,9 @@ } }, "node_modules/autoprefixer/node_modules/browserslist": { - "version": "4.23.3", - "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.23.3.tgz", - "integrity": "sha512-btwCFJVjI4YWDNfau8RhZ+B1Q/VLoUITrm3RlP6y1tYGWIOa+InuYiRGXUBXo8nA1qKmHMyLB/iVQg5TT4eFoA==", + "version": "4.24.0", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.24.0.tgz", + "integrity": "sha512-Rmb62sR1Zpjql25eSanFGEhAxcFwfA1K0GuQcLoaJBAcENegrQut3hYdhXFF1obQfiDyqIW/cLM5HSJ/9k884A==", "dev": true, "funding": [ { @@ -12004,8 +12416,8 @@ ], "license": "MIT", "dependencies": { - "caniuse-lite": "^1.0.30001646", - "electron-to-chromium": "^1.5.4", + "caniuse-lite": "^1.0.30001663", + "electron-to-chromium": "^1.5.28", "node-releases": "^2.0.18", "update-browserslist-db": "^1.1.0" }, @@ -12065,9 +12477,9 @@ } }, "node_modules/b4a": { - "version": "1.6.6", - "resolved": "https://registry.npmjs.org/b4a/-/b4a-1.6.6.tgz", - "integrity": "sha512-5Tk1HLk6b6ctmjIkAcU/Ujv/1WqiDl0F0JdRCR80VsOcUlHcu7pWeWRlOqQLHfDEsVx9YH/aif5AG4ehoCtTmg==", + "version": "1.6.7", + "resolved": "https://registry.npmjs.org/b4a/-/b4a-1.6.7.tgz", + "integrity": "sha512-OnAYlL5b7LEkALw87fUVafQw5rVR9RjwGd4KUwNQ6DrrNmaVaUCgLipfVlzrPQ4tWOR9P0IXGNOx50jYCCdSJg==", "dev": true, "license": "Apache-2.0" }, @@ -13287,9 +13699,9 @@ } }, "node_modules/caniuse-lite": { - "version": "1.0.30001663", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001663.tgz", - "integrity": "sha512-o9C3X27GLKbLeTYZ6HBOLU1tsAcBZsLis28wrVzddShCS16RujjHp9GDHKZqrB3meE0YjhawvMFsGb/igqiPzA==", + "version": "1.0.30001664", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001664.tgz", + "integrity": "sha512-AmE7k4dXiNKQipgn7a2xg558IRqPN3jMQY/rOsbxDhrd0tyChwbITBfiwtnqz8bi2M5mIWbxAYBvk7W7QBUS2g==", "funding": [ { "type": "opencollective", @@ -14640,9 +15052,9 @@ } }, "node_modules/core-js-compat/node_modules/browserslist": { - "version": "4.23.3", - "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.23.3.tgz", - "integrity": "sha512-btwCFJVjI4YWDNfau8RhZ+B1Q/VLoUITrm3RlP6y1tYGWIOa+InuYiRGXUBXo8nA1qKmHMyLB/iVQg5TT4eFoA==", + "version": "4.24.0", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.24.0.tgz", + "integrity": "sha512-Rmb62sR1Zpjql25eSanFGEhAxcFwfA1K0GuQcLoaJBAcENegrQut3hYdhXFF1obQfiDyqIW/cLM5HSJ/9k884A==", "dev": true, "funding": [ { @@ -14660,8 +15072,8 @@ ], "license": "MIT", "dependencies": { - "caniuse-lite": "^1.0.30001646", - "electron-to-chromium": "^1.5.4", + "caniuse-lite": "^1.0.30001663", + "electron-to-chromium": "^1.5.28", "node-releases": "^2.0.18", "update-browserslist-db": "^1.1.0" }, @@ -16227,9 +16639,9 @@ } }, "node_modules/electron-to-chromium": { - "version": "1.5.28", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.28.tgz", - "integrity": "sha512-VufdJl+rzaKZoYVUijN13QcXVF5dWPZANeFTLNy+OSpHdDL5ynXTF35+60RSBbaQYB1ae723lQXHCrf4pyLsMw==", + "version": "1.5.29", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.29.tgz", + "integrity": "sha512-PF8n2AlIhCKXQ+gTpiJi0VhcHDb69kYX4MtCiivctc2QD3XuNZ/XIOlbGzt7WAjjEev0TtaH6Cu3arZExm5DOw==", "license": "ISC" }, "node_modules/electron-updater": { @@ -16980,9 +17392,9 @@ } }, "node_modules/eslint-module-utils": { - "version": "2.11.1", - "resolved": "https://registry.npmjs.org/eslint-module-utils/-/eslint-module-utils-2.11.1.tgz", - "integrity": "sha512-EwcbfLOhwVMAfatfqLecR2yv3dE5+kQ8kx+Rrt0DvDXEVwW86KQ/xbMDQhtp5l42VXukD5SOF8mQQHbaNtO0CQ==", + "version": "2.12.0", + "resolved": "https://registry.npmjs.org/eslint-module-utils/-/eslint-module-utils-2.12.0.tgz", + "integrity": "sha512-wALZ0HFoytlyh/1+4wuZ9FJCD/leWHQzzrxJ8+rebyReSLk7LApMyd3WJaLVoN+D5+WIdJyDK1c6JnE65V4Zyg==", "dev": true, "license": "MIT", "dependencies": { @@ -17992,9 +18404,9 @@ "license": "MIT" }, "node_modules/fast-uri": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.0.1.tgz", - "integrity": "sha512-MWipKbbYiYI0UC7cl8m/i/IWTqfC8YXsqjzybjddLsFjStroQzsHXkc73JutMvBiXmOvapk+axIl79ig5t55Bw==", + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.0.2.tgz", + "integrity": "sha512-GR6f0hD7XXyNJa25Tb9BuIdN0tdr+0BMi6/CJPH3wJO1JjNG3n/VsSw38AwRdKZABm8lGbPfakLRkYzx2V9row==", "dev": true, "license": "MIT" }, @@ -18566,9 +18978,9 @@ "license": "ISC" }, "node_modules/flow-parser": { - "version": "0.246.0", - "resolved": "https://registry.npmjs.org/flow-parser/-/flow-parser-0.246.0.tgz", - "integrity": "sha512-WHRizzSrWFTcKo7cVcbP3wzZVhzsoYxoWqbnH4z+JXGqrjVmnsld6kBZWVlB200PwD5ur8r+HV3KUDxv3cHhOQ==", + "version": "0.247.1", + "resolved": "https://registry.npmjs.org/flow-parser/-/flow-parser-0.247.1.tgz", + "integrity": "sha512-DHwcm06fWbn2Z6uFD3NaBZ5lMOoABIQ4asrVA80IWvYjjT5WdbghkUOL1wIcbLcagnFTdCZYOlSNnKNp/xnRZQ==", "dev": true, "license": "MIT", "engines": { @@ -20752,9 +21164,9 @@ } }, "node_modules/hast-util-to-string": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/hast-util-to-string/-/hast-util-to-string-3.0.0.tgz", - "integrity": "sha512-OGkAxX1Ua3cbcW6EJ5pT/tslVb90uViVkcJ4ZZIMW/R33DX/AkcJcRrPebPwJkHYwlDHXz4aIwvAAaAdtrACFA==", + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/hast-util-to-string/-/hast-util-to-string-3.0.1.tgz", + "integrity": "sha512-XelQVTDWvqcl3axRfI0xSeoVKzyIFPwsAGSLIsKdJKQMXDYJS4WYrBNF/8J7RdhIcFI2BOHgAifggsvsxp/3+A==", "dev": true, "license": "MIT", "dependencies": { @@ -24317,12 +24729,12 @@ } }, "node_modules/jsdom": { - "version": "24.1.3", - "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-24.1.3.tgz", - "integrity": "sha512-MyL55p3Ut3cXbeBEG7Hcv0mVM8pp8PBNWxRqchZnSfAiES1v1mRnMeFfaHWIPULpwsYfvO+ZmMZz5tGCnjzDUQ==", + "version": "25.0.1", + "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-25.0.1.tgz", + "integrity": "sha512-8i7LzZj7BF8uplX+ZyOlIz86V6TAsSs+np6m1kpW9u0JWi4z/1t+FzcK1aek+ybTnAC4KhBL4uXCNT0wcUIeCw==", "license": "MIT", "dependencies": { - "cssstyle": "^4.0.1", + "cssstyle": "^4.1.0", "data-urls": "^5.0.0", "decimal.js": "^10.4.3", "form-data": "^4.0.0", @@ -24335,7 +24747,7 @@ "rrweb-cssom": "^0.7.1", "saxes": "^6.0.0", "symbol-tree": "^3.2.4", - "tough-cookie": "^4.1.4", + "tough-cookie": "^5.0.0", "w3c-xmlserializer": "^5.0.0", "webidl-conversions": "^7.0.0", "whatwg-encoding": "^3.1.1", @@ -24381,6 +24793,18 @@ "node": ">= 14" } }, + "node_modules/jsdom/node_modules/tough-cookie": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-5.0.0.tgz", + "integrity": "sha512-FRKsF7cz96xIIeMZ82ehjC3xW2E+O2+v11udrDYewUbszngYhsGa8z6YUMMzO9QJZzzyd0nGGXnML/TReX6W8Q==", + "license": "BSD-3-Clause", + "dependencies": { + "tldts": "^6.1.32" + }, + "engines": { + "node": ">=16" + } + }, "node_modules/jsesc": { "version": "2.5.2", "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-2.5.2.tgz", @@ -28865,15 +29289,15 @@ } }, "node_modules/nwsapi": { - "version": "2.2.12", - "resolved": "https://registry.npmjs.org/nwsapi/-/nwsapi-2.2.12.tgz", - "integrity": "sha512-qXDmcVlZV4XRtKFzddidpfVP4oMSGhga+xdMc25mv8kaLUHtgzCDhUxkrN8exkGdTlLNaXj7CV3GtON7zuGZ+w==", + "version": "2.2.13", + "resolved": "https://registry.npmjs.org/nwsapi/-/nwsapi-2.2.13.tgz", + "integrity": "sha512-cTGB9ptp9dY9A5VbMSe7fQBcl/tt22Vcqdq8+eN93rblOuE0aCFu4aZ2vMwct/2t+lFnosm8RkQW1I0Omb1UtQ==", "license": "MIT" }, "node_modules/nypm": { - "version": "0.3.11", - "resolved": "https://registry.npmjs.org/nypm/-/nypm-0.3.11.tgz", - "integrity": "sha512-E5GqaAYSnbb6n1qZyik2wjPDZON43FqOJO59+3OkWrnmQtjggrMOVnsyzfjxp/tS6nlYJBA4zRA5jSM2YaadMg==", + "version": "0.3.12", + "resolved": "https://registry.npmjs.org/nypm/-/nypm-0.3.12.tgz", + "integrity": "sha512-D3pzNDWIvgA+7IORhD/IuWzEk4uXv6GsgOxiid4UU3h9oq5IqV1KtPDi63n4sZJ/xcWlr88c0QM2RgN5VbOhFA==", "dev": true, "license": "MIT", "dependencies": { @@ -29626,9 +30050,9 @@ } }, "node_modules/package-json-from-dist": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.0.tgz", - "integrity": "sha512-dATvCeZN/8wQsGywez1mzHtTlP22H8OEfPrVMLNr4/eGa+ijtLn/6M5f0dY8UKNrC2O9UCU6SSoG3qRKnt7STw==", + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz", + "integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==", "dev": true, "license": "BlueOak-1.0.0" }, @@ -30955,9 +31379,9 @@ } }, "node_modules/prettier-plugin-tailwindcss": { - "version": "0.6.6", - "resolved": "https://registry.npmjs.org/prettier-plugin-tailwindcss/-/prettier-plugin-tailwindcss-0.6.6.tgz", - "integrity": "sha512-OPva5S7WAsPLEsOuOWXATi13QrCKACCiIonFgIR6V4lYv4QLp++UXVhZSzRbZxXGimkQtQT86CC6fQqTOybGng==", + "version": "0.6.8", + "resolved": "https://registry.npmjs.org/prettier-plugin-tailwindcss/-/prettier-plugin-tailwindcss-0.6.8.tgz", + "integrity": "sha512-dGu3kdm7SXPkiW4nzeWKCl3uoImdd5CTZEJGxyypEPL37Wj0HT2pLqjrvSei1nTeuQfO4PUfjeW5cTUNRLZ4sA==", "dev": true, "license": "MIT", "engines": { @@ -35632,9 +36056,9 @@ } }, "node_modules/terser-webpack-plugin/node_modules/terser": { - "version": "5.33.0", - "resolved": "https://registry.npmjs.org/terser/-/terser-5.33.0.tgz", - "integrity": "sha512-JuPVaB7s1gdFKPKTelwUyRq5Sid2A3Gko2S0PncwdBq7kN9Ti9HPWDQ06MPsEDGsZeVESjKEnyGy68quBk1w6g==", + "version": "5.34.1", + "resolved": "https://registry.npmjs.org/terser/-/terser-5.34.1.tgz", + "integrity": "sha512-FsJZ7iZLd/BXkz+4xrRTGJ26o/6VTjQytUk8b8OxkwcD2I+79VPJlz7qss1+zE7h8GNIScFqXcDyJ/KqBYZFVA==", "dev": true, "license": "BSD-2-Clause", "dependencies": { @@ -35912,21 +36336,21 @@ } }, "node_modules/tldts": { - "version": "6.1.46", - "resolved": "https://registry.npmjs.org/tldts/-/tldts-6.1.46.tgz", - "integrity": "sha512-fw81lXV2CijkNrZAZvee7wegs+EOlTyIuVl/z4q6OUzZHQ1jGL2xQzKXq9geYf/1tzo9LZQLrkcko2m8HLh+rg==", + "version": "6.1.48", + "resolved": "https://registry.npmjs.org/tldts/-/tldts-6.1.48.tgz", + "integrity": "sha512-SPbnh1zaSzi/OsmHb1vrPNnYuwJbdWjwo5TbBYYMlTtH3/1DSb41t8bcSxkwDmmbG2q6VLPVvQc7Yf23T+1EEw==", "license": "MIT", "dependencies": { - "tldts-core": "^6.1.46" + "tldts-core": "^6.1.48" }, "bin": { "tldts": "bin/cli.js" } }, "node_modules/tldts-core": { - "version": "6.1.47", - "resolved": "https://registry.npmjs.org/tldts-core/-/tldts-core-6.1.47.tgz", - "integrity": "sha512-6SWyFMnlst1fEt7GQVAAu16EGgFK0cLouH/2Mk6Ftlwhv3Ol40L0dlpGMcnnNiiOMyD2EV/aF3S+U2nKvvLvrA==", + "version": "6.1.48", + "resolved": "https://registry.npmjs.org/tldts-core/-/tldts-core-6.1.48.tgz", + "integrity": "sha512-3gD9iKn/n2UuFH1uilBviK9gvTNT6iYwdqrj1Vr5mh8FuelvpRNaYVH4pNYqUgOGU4aAdL9X35eLuuj0gRsx+A==", "license": "MIT" }, "node_modules/tmp": { @@ -37322,9 +37746,9 @@ } }, "node_modules/update-browserslist-db": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.0.tgz", - "integrity": "sha512-EdRAaAyk2cUE1wOf2DkEhzxqOQvFOoRJFNS6NeyJ01Gp2beMRpBAINjM2iDXE3KCuKhwnvHIQCJm6ThL2Z+HzQ==", + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.1.tgz", + "integrity": "sha512-R8UzCaa9Az+38REPiJ1tXlImTJXlVfgHZsglwBD/k6nj76ctsH1E3q4doGrukiLQd3sGQYu56r5+lo5r94l29A==", "funding": [ { "type": "opencollective", @@ -37341,8 +37765,8 @@ ], "license": "MIT", "dependencies": { - "escalade": "^3.1.2", - "picocolors": "^1.0.1" + "escalade": "^3.2.0", + "picocolors": "^1.1.0" }, "bin": { "update-browserslist-db": "cli.js" diff --git a/package.json b/package.json index a9d4f18e2fd..115b02eebe2 100644 --- a/package.json +++ b/package.json @@ -44,7 +44,7 @@ "@babel/core": "7.24.9", "@babel/preset-env": "7.24.8", "@compodoc/compodoc": "1.1.25", - "@electron/notarize": "2.4.0", + "@electron/notarize": "2.5.0", "@electron/rebuild": "3.6.0", "@ngtools/webpack": "16.2.14", "@storybook/addon-a11y": "8.2.9", @@ -58,7 +58,7 @@ "@storybook/theming": "8.2.9", "@types/argon2-browser": "1.18.4", "@types/chrome": "0.0.272", - "@types/firefox-webext-browser": "111.0.5", + "@types/firefox-webext-browser": "120.0.4", "@types/inquirer": "8.2.10", "@types/jest": "29.5.12", "@types/jquery": "3.5.30", @@ -70,7 +70,7 @@ "@types/koa-json": "2.0.23", "@types/lowdb": "1.0.15", "@types/lunr": "2.3.7", - "@types/node": "20.16.5", + "@types/node": "20.16.10", "@types/node-fetch": "2.6.4", "@types/node-forge": "1.3.11", "@types/node-ipc": "9.2.3", @@ -124,7 +124,7 @@ "postcss": "8.4.38", "postcss-loader": "8.1.1", "prettier": "3.3.3", - "prettier-plugin-tailwindcss": "0.6.6", + "prettier-plugin-tailwindcss": "0.6.8", "process": "0.11.10", "regedit": "3.0.3", "remark-gfm": "4.0.0", @@ -157,6 +157,7 @@ "@angular/platform-browser": "16.2.12", "@angular/platform-browser-dynamic": "16.2.12", "@angular/router": "16.2.12", + "@electron/fuses": "1.8.0", "@koa/multer": "3.0.2", "@koa/router": "12.0.1", "@microsoft/signalr": "8.0.7", @@ -176,7 +177,7 @@ "https-proxy-agent": "7.0.5", "inquirer": "8.2.6", "jquery": "3.7.1", - "jsdom": "24.1.3", + "jsdom": "25.0.1", "jszip": "3.10.1", "koa": "2.15.0", "koa-bodyparser": "4.4.1", @@ -199,7 +200,7 @@ "qrious": "4.0.2", "rxjs": "7.8.1", "tabbable": "6.2.0", - "tldts": "6.1.46", + "tldts": "6.1.48", "utf-8-validate": "6.0.4", "zone.js": "0.13.3", "zxcvbn": "4.4.2" @@ -210,7 +211,9 @@ "zone.js": "$zone.js" }, "replacestream": "4.0.3", - "@types/minimatch": "3.0.5" + "@electron/asar": { + "@types/glob": "7.1.3" + } }, "lint-staged": { "*": "prettier --cache --ignore-unknown --write", diff --git a/tsconfig.json b/tsconfig.json index 46829a9c30f..6764610d2f4 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -31,6 +31,7 @@ "@bitwarden/vault-export-ui": ["./libs/tools/export/vault-export/vault-export-ui/src"], "@bitwarden/importer/core": ["./libs/importer/src"], "@bitwarden/importer/ui": ["./libs/importer/src/components"], + "@bitwarden/key-management": ["./libs/key-management/src"], "@bitwarden/platform": ["./libs/platform/src"], "@bitwarden/send-ui": ["./libs/tools/send/send-ui/src"], "@bitwarden/node/*": ["./libs/node/src/*"],