1
0
mirror of https://github.com/bitwarden/browser.git synced 2024-11-29 12:55:21 +01:00

Merge branch 'main' into vault/pm-5273

# Conflicts:
#	apps/browser/src/popup/app.component.ts
#	libs/common/src/state-migrations/migrate.ts
This commit is contained in:
Carlos Gonçalves 2024-03-12 20:05:58 +00:00
commit 0e5f66e9c7
No known key found for this signature in database
GPG Key ID: 8147F618E732EF25
129 changed files with 3197 additions and 1250 deletions

View File

@ -37,7 +37,6 @@
"base64-loader", "base64-loader",
"buffer", "buffer",
"bufferutil", "bufferutil",
"clean-webpack-plugin",
"copy-webpack-plugin", "copy-webpack-plugin",
"core-js", "core-js",
"css-loader", "css-loader",

View File

@ -6,37 +6,11 @@ on:
tags: tags:
- desktop-v** - desktop-v**
defaults:
run:
shell: bash
jobs: jobs:
bump-version: bump-version:
name: Bump Desktop Version name: Bump Desktop Version
runs-on: ubuntu-22.04 runs-on: ubuntu-22.04
steps: steps:
- name: Checkout Branch
uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
- name: Calculate bumped version
id: version
env:
RELEASE_TAG: ${{ github.ref }}
run: |
CURR_MAJOR=$(echo $RELEASE_TAG | sed -r 's/refs\/tags\/[a-z]*-v([0-9]{4}\.[0-9]{1,2})\.([0-9]{1,2})/\1/')
CURR_PATCH=$(echo $RELEASE_TAG | sed -r 's/refs\/tags\/[a-z]*-v([0-9]{4}\.[0-9]{1,2})\.([0-9]{1,2})/\2/')
echo "Current Major: $CURR_MAJOR"
echo "Current Patch: $CURR_PATCH"
NEW_PATCH=$((CURR_PATCH+1))
echo "New patch: $NEW_PATCH"
NEW_VER=$CURR_MAJOR.$NEW_PATCH
echo "New Version: $NEW_VER"
echo "New Version: $NEW_VER" >> $GITHUB_STEP_SUMMARY
echo "new_version=$NEW_VER" >> $GITHUB_OUTPUT
- name: Login to Azure - CI Subscription - name: Login to Azure - CI Subscription
uses: Azure/login@e15b166166a8746d1a47596803bd8c1b595455cf # v1.6.0 uses: Azure/login@e15b166166a8746d1a47596803bd8c1b595455cf # v1.6.0
with: with:
@ -49,10 +23,13 @@ jobs:
keyvault: bitwarden-ci keyvault: bitwarden-ci
secrets: "github-pat-bitwarden-devops-bot-repo-scope" secrets: "github-pat-bitwarden-devops-bot-repo-scope"
- name: "Bump version to ${{ steps.version.outputs.new_version }}" - name: Trigger Version Bump workflow
env: env:
GH_TOKEN: ${{ steps.retrieve-bot-secrets.outputs.github-pat-bitwarden-devops-bot-repo-scope }} GH_TOKEN: ${{ steps.retrieve-bot-secrets.outputs.github-pat-bitwarden-devops-bot-repo-scope }}
run: | run: |
echo '{"cut_rc_branch": "false", "version_number": "${{ steps.version.outputs.new_version }}", echo '{"cut_rc_branch": "false", \
"bump_browser": "false", "bump_cli": "false", "bump_desktop": "true", "bump_web": "false"}' | \ "bump_browser": "false", \
"bump_cli": "false", \
"bump_desktop": "true", \
"bump_web": "false"}' | \
gh workflow run version-bump.yml --json --repo bitwarden/clients gh workflow run version-bump.yml --json --repo bitwarden/clients

View File

@ -1,6 +1,5 @@
--- ---
name: Version Bump name: Version Bump
run-name: Version Bump - v${{ inputs.version_number }}
on: on:
workflow_dispatch: workflow_dispatch:
@ -21,9 +20,10 @@ on:
description: "Bump Web?" description: "Bump Web?"
type: boolean type: boolean
default: false default: false
version_number: version_number_override:
description: "New version (example: '2024.1.0')" description: "New version override (leave blank for automatic calculation, example: '2024.1.0')"
required: true required: false
type: string
cut_rc_branch: cut_rc_branch:
description: "Cut RC branch?" description: "Cut RC branch?"
default: true default: true
@ -31,22 +31,19 @@ on:
jobs: jobs:
bump_version: bump_version:
name: "Bump Version to v${{ inputs.version_number }}" name: Bump Version
runs-on: ubuntu-22.04 runs-on: ubuntu-22.04
outputs:
version_browser: ${{ steps.set-final-version-output.outputs.version_browser }}
version_cli: ${{ steps.set-final-version-output.outputs.version_cli }}
version_desktop: ${{ steps.set-final-version-output.outputs.version_desktop }}
version_web: ${{ steps.set-final-version-output.outputs.version_web }}
steps: steps:
- name: Login to Azure - Prod Subscription - name: Validate version input
uses: Azure/login@e15b166166a8746d1a47596803bd8c1b595455cf # v1.6.0 if: ${{ inputs.version_number_override != '' }}
uses: bitwarden/gh-actions/version-check@main
with: with:
creds: ${{ secrets.AZURE_KV_CI_SERVICE_PRINCIPAL }} version: ${{ inputs.version_number_override }}
- name: Retrieve secrets
id: retrieve-secrets
uses: bitwarden/gh-actions/get-keyvault-secrets@main
with:
keyvault: "bitwarden-ci"
secrets: "github-gpg-private-key,
github-gpg-private-key-passphrase,
github-pat-bitwarden-devops-bot-repo-scope"
- name: Checkout Branch - name: Checkout Branch
uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
@ -63,6 +60,20 @@ jobs:
exit 1 exit 1
fi fi
- name: Login to Azure - CI Subscription
uses: Azure/login@e15b166166a8746d1a47596803bd8c1b595455cf # v1.6.0
with:
creds: ${{ secrets.AZURE_KV_CI_SERVICE_PRINCIPAL }}
- name: Retrieve secrets
id: retrieve-secrets
uses: bitwarden/gh-actions/get-keyvault-secrets@main
with:
keyvault: "bitwarden-ci"
secrets: "github-gpg-private-key,
github-gpg-private-key-passphrase,
github-pat-bitwarden-devops-bot-repo-scope"
- name: Import GPG key - name: Import GPG key
uses: crazy-max/ghaction-import-gpg@01dd5d3ca463c7f10f7f4f7b4f177225ac661ee4 # v6.1.0 uses: crazy-max/ghaction-import-gpg@01dd5d3ca463c7f10f7f4f7b4f177225ac661ee4 # v6.1.0
with: with:
@ -71,6 +82,11 @@ jobs:
git_user_signingkey: true git_user_signingkey: true
git_commit_gpgsign: true git_commit_gpgsign: true
- name: Setup git
run: |
git config --local user.email "106330231+bitwarden-devops-bot@users.noreply.github.com"
git config --local user.name "bitwarden-devops-bot"
- name: Create Version Branch - name: Create Version Branch
id: create-branch id: create-branch
run: | run: |
@ -90,7 +106,7 @@ jobs:
printf -v joined '%s,' "${CLIENTS[@]}" printf -v joined '%s,' "${CLIENTS[@]}"
echo "client=${joined%,}" >> $GITHUB_OUTPUT echo "client=${joined%,}" >> $GITHUB_OUTPUT
NAME=version_bump_${{ github.ref_name }}_${{ inputs.version_number }} NAME=version_bump_${{ github.ref_name }}_$(date +"%Y-%m-%d")
git switch -c $NAME git switch -c $NAME
echo "name=$NAME" >> $GITHUB_OUTPUT echo "name=$NAME" >> $GITHUB_OUTPUT
@ -99,13 +115,20 @@ jobs:
######################## ########################
### Browser ### Browser
- name: Browser - Verify input version - name: Get current Browser version
if: ${{ inputs.bump_browser == true }} if: ${{ inputs.bump_browser == true }}
env: id: current-browser-version
NEW_VERSION: ${{ inputs.version_number }}
run: | run: |
CURRENT_VERSION=$(cat package.json | jq -r '.version') CURRENT_VERSION=$(cat package.json | jq -r '.version')
echo "version=$CURRENT_VERSION" >> $GITHUB_OUTPUT
working-directory: apps/browser
- name: Browser - Verify input version
if: ${{ inputs.bump_browser == true && inputs.version_number_override != '' }}
env:
CURRENT_VERSION: ${{ steps.current-browser-version.outputs.version }}
NEW_VERSION: ${{ inputs.version_number_override }}
run: |
# Error if version has not changed. # Error if version has not changed.
if [[ "$NEW_VERSION" == "$CURRENT_VERSION" ]]; then if [[ "$NEW_VERSION" == "$CURRENT_VERSION" ]]; then
echo "Version has not changed." echo "Version has not changed."
@ -122,23 +145,52 @@ jobs:
fi fi
working-directory: apps/browser working-directory: apps/browser
- name: Bump Browser Version - name: Calculate next Browser release version
if: ${{ inputs.bump_browser == true }} if: ${{ inputs.bump_browser == true && inputs.version_number_override == '' }}
run: npm version --workspace=@bitwarden/browser ${{ inputs.version_number }} id: calculate-next-browser-version
uses: bitwarden/gh-actions/version-next@main
with:
version: ${{ steps.current-browser-version.outputs.version }}
- name: Bump Browser Version - Manifest - name: Bump Browser Version - Version Override
if: ${{ inputs.bump_browser == true }} if: ${{ inputs.bump_browser == true && inputs.version_number_override != '' }}
id: bump-browser-version-override
run: npm version --workspace=@bitwarden/browser ${{ inputs.version_number_override }}
- name: Bump Browser Version - Automatic Calculation
if: ${{ inputs.bump_browser == true && inputs.version_number_override == '' }}
id: bump-browser-version-automatic
env:
VERSION: ${{ steps.calculate-next-browser-version.outputs.version }}
run: npm version --workspace=@bitwarden/browser $VERSION
- name: Bump Browser Version - Manifest - Version Override
if: ${{ inputs.bump_browser == true && inputs.version_number_override != '' }}
uses: bitwarden/gh-actions/version-bump@main uses: bitwarden/gh-actions/version-bump@main
with: with:
version: ${{ inputs.version_number }}
file_path: "apps/browser/src/manifest.json" file_path: "apps/browser/src/manifest.json"
version: ${{ inputs.version_number_override }}
- name: Bump Browser Version - Manifest v3 - name: Bump Browser Version - Manifest - Automatic Calculation
if: ${{ inputs.bump_browser == true }} if: ${{ inputs.bump_browser == true && inputs.version_number_override == '' }}
uses: bitwarden/gh-actions/version-bump@main
with:
file_path: "apps/browser/src/manifest.json"
version: ${{ steps.calculate-next-browser-version.outputs.version }}
- name: Bump Browser Version - Manifest v3 - Version Override
if: ${{ inputs.bump_browser == true && inputs.version_number_override != '' }}
uses: bitwarden/gh-actions/version-bump@main uses: bitwarden/gh-actions/version-bump@main
with: with:
version: ${{ inputs.version_number }}
file_path: "apps/browser/src/manifest.v3.json" file_path: "apps/browser/src/manifest.v3.json"
version: ${{ inputs.version_number_override }}
- name: Bump Browser Version - Manifest v3 - Automatic Calculation
if: ${{ inputs.bump_browser == true && inputs.version_number_override == '' }}
uses: bitwarden/gh-actions/version-bump@main
with:
file_path: "apps/browser/src/manifest.v3.json"
version: ${{ steps.calculate-next-browser-version.outputs.version }}
- name: Run Prettier after Browser Version Bump - name: Run Prettier after Browser Version Bump
if: ${{ inputs.bump_browser == true }} if: ${{ inputs.bump_browser == true }}
@ -148,13 +200,20 @@ jobs:
prettier --write apps/browser/src/manifest.v3.json prettier --write apps/browser/src/manifest.v3.json
### CLI ### CLI
- name: CLI - Verify input version - name: Get current CLI version
if: ${{ inputs.bump_cli == true }} if: ${{ inputs.bump_cli == true }}
env: id: current-cli-version
NEW_VERSION: ${{ inputs.version_number }}
run: | run: |
CURRENT_VERSION=$(cat package.json | jq -r '.version') CURRENT_VERSION=$(cat package.json | jq -r '.version')
echo "version=$CURRENT_VERSION" >> $GITHUB_OUTPUT
working-directory: apps/cli
- name: CLI - Verify input version
if: ${{ inputs.bump_cli == true && inputs.version_number_override != '' }}
env:
CURRENT_VERSION: ${{ steps.current-cli-version.outputs.version }}
NEW_VERSION: ${{ inputs.version_number_override }}
run: |
# Error if version has not changed. # Error if version has not changed.
if [[ "$NEW_VERSION" == "$CURRENT_VERSION" ]]; then if [[ "$NEW_VERSION" == "$CURRENT_VERSION" ]]; then
echo "Version has not changed." echo "Version has not changed."
@ -171,18 +230,40 @@ jobs:
fi fi
working-directory: apps/cli working-directory: apps/cli
- name: Bump CLI Version - name: Calculate next CLI release version
if: ${{ inputs.bump_cli == true }} if: ${{ inputs.bump_cli == true && inputs.version_number_override == '' }}
run: npm version --workspace=@bitwarden/cli ${{ inputs.version_number }} id: calculate-next-cli-version
uses: bitwarden/gh-actions/version-next@main
with:
version: ${{ steps.current-cli-version.outputs.version }}
- name: Bump CLI Version - Version Override
if: ${{ inputs.bump_cli == true && inputs.version_number_override != '' }}
id: bump-cli-version-override
run: npm version --workspace=@bitwarden/cli ${{ inputs.version_number_override }}
- name: Bump CLI Version - Automatic Calculation
if: ${{ inputs.bump_cli == true && inputs.version_number_override == '' }}
id: bump-cli-version-automatic
env:
VERSION: ${{ steps.calculate-next-cli-version.outputs.version }}
run: npm version --workspace=@bitwarden/cli $VERSION
### Desktop ### Desktop
- name: Desktop - Verify input version - name: Get current Desktop version
if: ${{ inputs.bump_desktop == true }} if: ${{ inputs.bump_desktop == true }}
env: id: current-desktop-version
NEW_VERSION: ${{ inputs.version_number }}
run: | run: |
CURRENT_VERSION=$(cat package.json | jq -r '.version') CURRENT_VERSION=$(cat package.json | jq -r '.version')
echo "version=$CURRENT_VERSION" >> $GITHUB_OUTPUT
working-directory: apps/desktop
- name: Desktop - Verify input version
if: ${{ inputs.bump_desktop == true && inputs.version_number_override != '' }}
env:
CURRENT_VERSION: ${{ steps.current-desktop-version.outputs.version }}
NEW_VERSION: ${{ inputs.version_number_override }}
run: |
# Error if version has not changed. # Error if version has not changed.
if [[ "$NEW_VERSION" == "$CURRENT_VERSION" ]]; then if [[ "$NEW_VERSION" == "$CURRENT_VERSION" ]]; then
echo "Version has not changed." echo "Version has not changed."
@ -199,23 +280,52 @@ jobs:
fi fi
working-directory: apps/desktop working-directory: apps/desktop
- name: Bump Desktop Version - Root - name: Calculate next Desktop release version
if: ${{ inputs.bump_desktop == true }} if: ${{ inputs.bump_desktop == true && inputs.version_number_override == '' }}
run: npm version --workspace=@bitwarden/desktop ${{ inputs.version_number }} id: calculate-next-desktop-version
uses: bitwarden/gh-actions/version-next@main
with:
version: ${{ steps.current-desktop-version.outputs.version }}
- name: Bump Desktop Version - App - name: Bump Desktop Version - Root - Version Override
if: ${{ inputs.bump_desktop == true }} if: ${{ inputs.bump_desktop == true && inputs.version_number_override != '' }}
run: npm version ${{ inputs.version_number }} id: bump-desktop-version-override
run: npm version --workspace=@bitwarden/desktop ${{ inputs.version_number_override }}
- name: Bump Desktop Version - Root - Automatic Calculation
if: ${{ inputs.bump_desktop == true && inputs.version_number_override == '' }}
id: bump-desktop-version-automatic
env:
VERSION: ${{ steps.calculate-next-desktop-version.outputs.version }}
run: npm version --workspace=@bitwarden/desktop $VERSION
- name: Bump Desktop Version - App - Version Override
if: ${{ inputs.bump_desktop == true && inputs.version_number_override != '' }}
run: npm version ${{ inputs.version_number_override }}
working-directory: "apps/desktop/src"
- name: Bump Desktop Version - App - Automatic Calculation
if: ${{ inputs.bump_desktop == true && inputs.version_number_override == '' }}
env:
VERSION: ${{ steps.calculate-next-desktop-version.outputs.version }}
run: npm version $VERSION
working-directory: "apps/desktop/src" working-directory: "apps/desktop/src"
### Web ### Web
- name: Web - Verify input version - name: Get current Web version
if: ${{ inputs.bump_web == true }} if: ${{ inputs.bump_web == true }}
env: id: current-web-version
NEW_VERSION: ${{ inputs.version_number }}
run: | run: |
CURRENT_VERSION=$(cat package.json | jq -r '.version') CURRENT_VERSION=$(cat package.json | jq -r '.version')
echo "version=$CURRENT_VERSION" >> $GITHUB_OUTPUT
working-directory: apps/web
- name: Web - Verify input version
if: ${{ inputs.bump_web == true && inputs.version_number_override != '' }}
env:
CURRENT_VERSION: ${{ steps.current-web-version.outputs.version }}
NEW_VERSION: ${{ inputs.version_number_override }}
run: |
# Error if version has not changed. # Error if version has not changed.
if [[ "$NEW_VERSION" == "$CURRENT_VERSION" ]]; then if [[ "$NEW_VERSION" == "$CURRENT_VERSION" ]]; then
echo "Version has not changed." echo "Version has not changed."
@ -232,16 +342,47 @@ jobs:
fi fi
working-directory: apps/web working-directory: apps/web
- name: Bump Web Version - name: Calculate next Web release version
if: ${{ inputs.bump_web == true }} if: ${{ inputs.bump_web == true && inputs.version_number_override == '' }}
run: npm version --workspace=@bitwarden/web-vault ${{ inputs.version_number }} id: calculate-next-web-version
uses: bitwarden/gh-actions/version-next@main
with:
version: ${{ steps.current-web-version.outputs.version }}
- name: Bump Web Version - Version Override
if: ${{ inputs.bump_web == true && inputs.version_number_override != '' }}
id: bump-web-version-override
run: npm version --workspace=@bitwarden/web-vault ${{ inputs.version_number_override }}
- name: Bump Web Version - Automatic Calculation
if: ${{ inputs.bump_web == true && inputs.version_number_override == '' }}
id: bump-web-version-automatic
env:
VERSION: ${{ steps.calculate-next-web-version.outputs.version }}
run: npm version --workspace=@bitwarden/web-vault $VERSION
######################## ########################
- name: Setup git - name: Set final version output
id: set-final-version-output
run: | run: |
git config --local user.email "106330231+bitwarden-devops-bot@users.noreply.github.com" if [[ "${{ steps.bump-browser-version-override.outcome }}" = "success" ]]; then
git config --local user.name "bitwarden-devops-bot" echo "version=${{ inputs.version_number_override }}" >> $GITHUB_OUTPUT
elif [[ "${{ steps.bump-browser-version-automatic.outcome }}" = "success" ]]; then
echo "version=${{ steps.calculate-next-browser-version.outputs.version }}" >> $GITHUB_OUTPUT
elif [[ "${{ steps.bump-cli-version-override.outcome }}" = "success" ]]; then
echo "version=${{ inputs.version_number_override }}" >> $GITHUB_OUTPUT
elif [[ "${{ steps.bump-cli-version-automatic.outcome }}" = "success" ]]; then
echo "version=${{ steps.calculate-next-cli-version.outputs.version }}" >> $GITHUB_OUTPUT
elif [[ "${{ steps.bump-desktop-version-override.outcome }}" = "success" ]]; then
echo "version=${{ inputs.version_number_override }}" >> $GITHUB_OUTPUT
elif [[ "${{ steps.bump-desktop-version-automatic.outcome }}" = "success" ]]; then
echo "version=${{ steps.calculate-next-desktop-version.outputs.version }}" >> $GITHUB_OUTPUT
elif [[ "${{ steps.bump-web-version-override.outcome }}" = "success" ]]; then
echo "version=${{ inputs.version_number_override }}" >> $GITHUB_OUTPUT
elif [[ "${{ steps.bump-web-version-automatic.outcome }}" = "success" ]]; then
echo "version=${{ steps.calculate-next-web-version.outputs.version }}" >> $GITHUB_OUTPUT
fi
- name: Check if version changed - name: Check if version changed
id: version-changed id: version-changed
@ -257,7 +398,7 @@ jobs:
if: ${{ steps.version-changed.outputs.changes_to_commit == 'TRUE' }} if: ${{ steps.version-changed.outputs.changes_to_commit == 'TRUE' }}
env: env:
CLIENT: ${{ steps.create-branch.outputs.client }} CLIENT: ${{ steps.create-branch.outputs.client }}
VERSION: ${{ inputs.version_number }} VERSION: ${{ steps.set-final-version-output.outputs.version }}
run: git commit -m "Bumped ${CLIENT} version to ${VERSION}" -a run: git commit -m "Bumped ${CLIENT} version to ${VERSION}" -a
- name: Push changes - name: Push changes
@ -272,7 +413,7 @@ jobs:
env: env:
GH_TOKEN: ${{ steps.retrieve-secrets.outputs.github-pat-bitwarden-devops-bot-repo-scope }} GH_TOKEN: ${{ steps.retrieve-secrets.outputs.github-pat-bitwarden-devops-bot-repo-scope }}
PR_BRANCH: ${{ steps.create-branch.outputs.name }} PR_BRANCH: ${{ steps.create-branch.outputs.name }}
TITLE: "Bump ${{ steps.create-branch.outputs.client }} version to ${{ inputs.version_number }}" TITLE: "Bump ${{ steps.create-branch.outputs.client }} version to ${{ steps.set-final-version-output.outputs.version }}"
run: | run: |
PR_URL=$(gh pr create --title "$TITLE" \ PR_URL=$(gh pr create --title "$TITLE" \
--base "main" \ --base "main" \
@ -288,7 +429,7 @@ jobs:
- [X] Other - [X] Other
## Objective ## Objective
Automated ${{ steps.create-branch.outputs.client }} version bump to ${{ inputs.version_number }}") Automated ${{ steps.create-branch.outputs.client }} version bump to ${{ steps.set-final-version-output.outputs.version }}")
echo "pr_number=${PR_URL##*/}" >> $GITHUB_OUTPUT echo "pr_number=${PR_URL##*/}" >> $GITHUB_OUTPUT
- name: Approve PR - name: Approve PR
@ -307,8 +448,8 @@ jobs:
cut_rc: cut_rc:
name: Cut RC branch name: Cut RC branch
needs: bump_version
if: ${{ inputs.cut_rc_branch == true }} if: ${{ inputs.cut_rc_branch == true }}
needs: bump_version
runs-on: ubuntu-22.04 runs-on: ubuntu-22.04
steps: steps:
- name: Checkout Branch - name: Checkout Branch
@ -320,7 +461,7 @@ jobs:
- name: Browser - Verify version has been updated - name: Browser - Verify version has been updated
if: ${{ inputs.bump_browser == true }} if: ${{ inputs.bump_browser == true }}
env: env:
NEW_VERSION: ${{ inputs.version_number }} NEW_VERSION: ${{ needs.bump_version.outputs.version_browser }}
run: | run: |
# Wait for version to change. # Wait for version to change.
while : ; do while : ; do
@ -338,7 +479,7 @@ jobs:
- name: CLI - Verify version has been updated - name: CLI - Verify version has been updated
if: ${{ inputs.bump_cli == true }} if: ${{ inputs.bump_cli == true }}
env: env:
NEW_VERSION: ${{ inputs.version_number }} NEW_VERSION: ${{ needs.bump_version.outputs.version_cli }}
run: | run: |
# Wait for version to change. # Wait for version to change.
while : ; do while : ; do
@ -356,7 +497,7 @@ jobs:
- name: Desktop - Verify version has been updated - name: Desktop - Verify version has been updated
if: ${{ inputs.bump_desktop == true }} if: ${{ inputs.bump_desktop == true }}
env: env:
NEW_VERSION: ${{ inputs.version_number }} NEW_VERSION: ${{ needs.bump_version.outputs.version_desktop }}
run: | run: |
# Wait for version to change. # Wait for version to change.
while : ; do while : ; do
@ -374,7 +515,7 @@ jobs:
- name: Web - Verify version has been updated - name: Web - Verify version has been updated
if: ${{ inputs.bump_web == true }} if: ${{ inputs.bump_web == true }}
env: env:
NEW_VERSION: ${{ inputs.version_number }} NEW_VERSION: ${{ needs.bump_version.outputs.version_web }}
run: | run: |
# Wait for version to change. # Wait for version to change.
while : ; do while : ; do

View File

@ -26,6 +26,10 @@ import {
factory, factory,
FactoryOptions, FactoryOptions,
} from "../../../platform/background/service-factories/factory-options"; } from "../../../platform/background/service-factories/factory-options";
import {
globalStateProviderFactory,
GlobalStateProviderInitOptions,
} from "../../../platform/background/service-factories/global-state-provider.factory";
import { import {
i18nServiceFactory, i18nServiceFactory,
I18nServiceInitOptions, I18nServiceInitOptions,
@ -84,7 +88,8 @@ export type LoginStrategyServiceInitOptions = LoginStrategyServiceFactoryOptions
PolicyServiceInitOptions & PolicyServiceInitOptions &
PasswordStrengthServiceInitOptions & PasswordStrengthServiceInitOptions &
DeviceTrustCryptoServiceInitOptions & DeviceTrustCryptoServiceInitOptions &
AuthRequestServiceInitOptions; AuthRequestServiceInitOptions &
GlobalStateProviderInitOptions;
export function loginStrategyServiceFactory( export function loginStrategyServiceFactory(
cache: { loginStrategyService?: LoginStrategyServiceAbstraction } & CachedServices, cache: { loginStrategyService?: LoginStrategyServiceAbstraction } & CachedServices,
@ -113,6 +118,7 @@ export function loginStrategyServiceFactory(
await policyServiceFactory(cache, opts), await policyServiceFactory(cache, opts),
await deviceTrustCryptoServiceFactory(cache, opts), await deviceTrustCryptoServiceFactory(cache, opts),
await authRequestServiceFactory(cache, opts), await authRequestServiceFactory(cache, opts),
await globalStateProviderFactory(cache, opts),
), ),
); );
} }

View File

@ -1,3 +1,4 @@
import { NeverDomains } from "@bitwarden/common/models/domain/domain-service";
import { FolderView } from "@bitwarden/common/vault/models/view/folder.view"; import { FolderView } from "@bitwarden/common/vault/models/view/folder.view";
import { NotificationQueueMessageTypes } from "../../enums/notification-queue-message-type.enum"; import { NotificationQueueMessageTypes } from "../../enums/notification-queue-message-type.enum";
@ -111,6 +112,7 @@ type NotificationBackgroundExtensionMessageHandlers = {
collectPageDetailsResponse: ({ message }: BackgroundMessageParam) => Promise<void>; collectPageDetailsResponse: ({ message }: BackgroundMessageParam) => Promise<void>;
bgGetEnableChangedPasswordPrompt: () => Promise<boolean>; bgGetEnableChangedPasswordPrompt: () => Promise<boolean>;
bgGetEnableAddedLoginPrompt: () => Promise<boolean>; bgGetEnableAddedLoginPrompt: () => Promise<boolean>;
bgGetExcludedDomains: () => Promise<NeverDomains>;
getWebVaultUrlForNotification: () => string; getWebVaultUrlForNotification: () => string;
}; };

View File

@ -4,6 +4,7 @@ import { firstValueFrom } from "rxjs";
import { PolicyService } from "@bitwarden/common/admin-console/services/policy/policy.service"; import { PolicyService } from "@bitwarden/common/admin-console/services/policy/policy.service";
import { AuthenticationStatus } from "@bitwarden/common/auth/enums/authentication-status"; import { AuthenticationStatus } from "@bitwarden/common/auth/enums/authentication-status";
import { AuthService } from "@bitwarden/common/auth/services/auth.service"; import { AuthService } from "@bitwarden/common/auth/services/auth.service";
import { DomainSettingsService } from "@bitwarden/common/autofill/services/domain-settings.service";
import { UserNotificationSettingsService } from "@bitwarden/common/autofill/services/user-notification-settings.service"; import { UserNotificationSettingsService } from "@bitwarden/common/autofill/services/user-notification-settings.service";
import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
import { EnvironmentService } from "@bitwarden/common/platform/services/environment.service"; import { EnvironmentService } from "@bitwarden/common/platform/services/environment.service";
@ -47,6 +48,7 @@ describe("NotificationBackground", () => {
const folderService = mock<FolderService>(); const folderService = mock<FolderService>();
const stateService = mock<BrowserStateService>(); const stateService = mock<BrowserStateService>();
const userNotificationSettingsService = mock<UserNotificationSettingsService>(); const userNotificationSettingsService = mock<UserNotificationSettingsService>();
const domainSettingsService = mock<DomainSettingsService>();
const environmentService = mock<EnvironmentService>(); const environmentService = mock<EnvironmentService>();
const logService = mock<LogService>(); const logService = mock<LogService>();
@ -59,6 +61,7 @@ describe("NotificationBackground", () => {
folderService, folderService,
stateService, stateService,
userNotificationSettingsService, userNotificationSettingsService,
domainSettingsService,
environmentService, environmentService,
logService, logService,
); );

View File

@ -5,7 +5,9 @@ import { PolicyType } from "@bitwarden/common/admin-console/enums";
import { AuthService } from "@bitwarden/common/auth/abstractions/auth.service"; import { AuthService } from "@bitwarden/common/auth/abstractions/auth.service";
import { AuthenticationStatus } from "@bitwarden/common/auth/enums/authentication-status"; import { AuthenticationStatus } from "@bitwarden/common/auth/enums/authentication-status";
import { NOTIFICATION_BAR_LIFESPAN_MS } from "@bitwarden/common/autofill/constants"; import { NOTIFICATION_BAR_LIFESPAN_MS } from "@bitwarden/common/autofill/constants";
import { DomainSettingsService } from "@bitwarden/common/autofill/services/domain-settings.service";
import { UserNotificationSettingsServiceAbstraction } from "@bitwarden/common/autofill/services/user-notification-settings.service"; import { UserNotificationSettingsServiceAbstraction } from "@bitwarden/common/autofill/services/user-notification-settings.service";
import { NeverDomains } from "@bitwarden/common/models/domain/domain-service";
import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service"; import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service";
import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
import { Utils } from "@bitwarden/common/platform/misc/utils"; import { Utils } from "@bitwarden/common/platform/misc/utils";
@ -60,6 +62,7 @@ export default class NotificationBackground {
bgReopenUnlockPopout: ({ sender }) => this.openUnlockPopout(sender.tab), bgReopenUnlockPopout: ({ sender }) => this.openUnlockPopout(sender.tab),
bgGetEnableChangedPasswordPrompt: () => this.getEnableChangedPasswordPrompt(), bgGetEnableChangedPasswordPrompt: () => this.getEnableChangedPasswordPrompt(),
bgGetEnableAddedLoginPrompt: () => this.getEnableAddedLoginPrompt(), bgGetEnableAddedLoginPrompt: () => this.getEnableAddedLoginPrompt(),
bgGetExcludedDomains: () => this.getExcludedDomains(),
getWebVaultUrlForNotification: () => this.getWebVaultUrl(), getWebVaultUrlForNotification: () => this.getWebVaultUrl(),
}; };
@ -71,6 +74,7 @@ export default class NotificationBackground {
private folderService: FolderService, private folderService: FolderService,
private stateService: BrowserStateService, private stateService: BrowserStateService,
private userNotificationSettingsService: UserNotificationSettingsServiceAbstraction, private userNotificationSettingsService: UserNotificationSettingsServiceAbstraction,
private domainSettingsService: DomainSettingsService,
private environmentService: EnvironmentService, private environmentService: EnvironmentService,
private logService: LogService, private logService: LogService,
) {} ) {}
@ -99,6 +103,13 @@ export default class NotificationBackground {
return await firstValueFrom(this.userNotificationSettingsService.enableAddedLoginPrompt$); return await firstValueFrom(this.userNotificationSettingsService.enableAddedLoginPrompt$);
} }
/**
* Gets the neverDomains setting from the domain settings service.
*/
async getExcludedDomains(): Promise<NeverDomains> {
return await firstValueFrom(this.domainSettingsService.neverDomains$);
}
/** /**
* Checks the notification queue for any messages that need to be sent to the * Checks the notification queue for any messages that need to be sent to the
* specified tab. If no tab is specified, the current tab will be used. * specified tab. If no tab is specified, the current tab will be used.

View File

@ -17,8 +17,8 @@ import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
import { CipherService } from "@bitwarden/common/vault/services/cipher.service"; import { CipherService } from "@bitwarden/common/vault/services/cipher.service";
import { BrowserApi } from "../../platform/browser/browser-api"; import { BrowserApi } from "../../platform/browser/browser-api";
import BrowserPlatformUtilsService from "../../platform/services/browser-platform-utils.service";
import { BrowserStateService } from "../../platform/services/browser-state.service"; import { BrowserStateService } from "../../platform/services/browser-state.service";
import { BrowserPlatformUtilsService } from "../../platform/services/platform-utils/browser-platform-utils.service";
import { AutofillService } from "../services/abstractions/autofill.service"; import { AutofillService } from "../services/abstractions/autofill.service";
import { import {
createAutofillPageDetailsMock, createAutofillPageDetailsMock,

View File

@ -6,10 +6,6 @@ import {
EventCollectionServiceInitOptions, EventCollectionServiceInitOptions,
eventCollectionServiceFactory, eventCollectionServiceFactory,
} from "../../../background/service-factories/event-collection-service.factory"; } from "../../../background/service-factories/event-collection-service.factory";
import {
settingsServiceFactory,
SettingsServiceInitOptions,
} from "../../../background/service-factories/settings-service.factory";
import { import {
CachedServices, CachedServices,
factory, factory,
@ -38,6 +34,10 @@ import {
AutofillSettingsServiceInitOptions, AutofillSettingsServiceInitOptions,
autofillSettingsServiceFactory, autofillSettingsServiceFactory,
} from "./autofill-settings-service.factory"; } from "./autofill-settings-service.factory";
import {
DomainSettingsServiceInitOptions,
domainSettingsServiceFactory,
} from "./domain-settings-service.factory";
type AutoFillServiceOptions = FactoryOptions; type AutoFillServiceOptions = FactoryOptions;
@ -48,8 +48,8 @@ export type AutoFillServiceInitOptions = AutoFillServiceOptions &
TotpServiceInitOptions & TotpServiceInitOptions &
EventCollectionServiceInitOptions & EventCollectionServiceInitOptions &
LogServiceInitOptions & LogServiceInitOptions &
SettingsServiceInitOptions & UserVerificationServiceInitOptions &
UserVerificationServiceInitOptions; DomainSettingsServiceInitOptions;
export function autofillServiceFactory( export function autofillServiceFactory(
cache: { autofillService?: AbstractAutoFillService } & CachedServices, cache: { autofillService?: AbstractAutoFillService } & CachedServices,
@ -67,7 +67,7 @@ export function autofillServiceFactory(
await totpServiceFactory(cache, opts), await totpServiceFactory(cache, opts),
await eventCollectionServiceFactory(cache, opts), await eventCollectionServiceFactory(cache, opts),
await logServiceFactory(cache, opts), await logServiceFactory(cache, opts),
await settingsServiceFactory(cache, opts), await domainSettingsServiceFactory(cache, opts),
await userVerificationServiceFactory(cache, opts), await userVerificationServiceFactory(cache, opts),
), ),
); );

View File

@ -0,0 +1,25 @@
import { DefaultDomainSettingsService } from "@bitwarden/common/autofill/services/domain-settings.service";
import {
CachedServices,
factory,
FactoryOptions,
} from "../../../platform/background/service-factories/factory-options";
import {
stateProviderFactory,
StateProviderInitOptions,
} from "../../../platform/background/service-factories/state-provider.factory";
export type DomainSettingsServiceInitOptions = FactoryOptions & StateProviderInitOptions;
export function domainSettingsServiceFactory(
cache: { domainSettingsService?: DefaultDomainSettingsService } & CachedServices,
opts: DomainSettingsServiceInitOptions,
): Promise<DefaultDomainSettingsService> {
return factory(
cache,
"domainSettingsService",
opts,
async () => new DefaultDomainSettingsService(await stateProviderFactory(cache, opts)),
);
}

View File

@ -1,8 +1,8 @@
import { AuthService } from "@bitwarden/common/auth/abstractions/auth.service"; import { AuthService } from "@bitwarden/common/auth/abstractions/auth.service";
import { AuthenticationStatus } from "@bitwarden/common/auth/enums/authentication-status"; import { AuthenticationStatus } from "@bitwarden/common/auth/enums/authentication-status";
import { UriMatchStrategy } from "@bitwarden/common/models/domain/domain-service";
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service"; import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
import { UriMatchType } from "@bitwarden/common/vault/enums";
import { BrowserApi } from "../../platform/browser/browser-api"; import { BrowserApi } from "../../platform/browser/browser-api";
@ -73,7 +73,7 @@ export default class WebRequestBackground {
const ciphers = await this.cipherService.getAllDecryptedForUrl( const ciphers = await this.cipherService.getAllDecryptedForUrl(
domain, domain,
null, null,
UriMatchType.Host, UriMatchStrategy.Host,
); );
if (ciphers == null || ciphers.length !== 1) { if (ciphers == null || ciphers.length !== 1) {
error(); error();

View File

@ -6,7 +6,7 @@ import AutofillField from "../models/autofill-field";
import { WatchedForm } from "../models/watched-form"; import { WatchedForm } from "../models/watched-form";
import { NotificationBarIframeInitData } from "../notification/abstractions/notification-bar"; import { NotificationBarIframeInitData } from "../notification/abstractions/notification-bar";
import { FormData } from "../services/abstractions/autofill.service"; import { FormData } from "../services/abstractions/autofill.service";
import { GlobalSettings, UserSettings } from "../types"; import { UserSettings } from "../types";
import { import {
getFromLocalStorage, getFromLocalStorage,
sendExtensionMessage, sendExtensionMessage,
@ -94,10 +94,11 @@ async function loadNotificationBar() {
"bgGetEnableChangedPasswordPrompt", "bgGetEnableChangedPasswordPrompt",
); );
const enableAddedLoginPrompt = await sendExtensionMessage("bgGetEnableAddedLoginPrompt"); const enableAddedLoginPrompt = await sendExtensionMessage("bgGetEnableAddedLoginPrompt");
const excludedDomains = await sendExtensionMessage("bgGetExcludedDomains");
let showNotificationBar = true; let showNotificationBar = true;
// Look up the active user id from storage // Look up the active user id from storage
const activeUserIdKey = "activeUserId"; const activeUserIdKey = "activeUserId";
const globalStorageKey = "global";
let activeUserId: string; let activeUserId: string;
const activeUserStorageValue = await getFromLocalStorage(activeUserIdKey); const activeUserStorageValue = await getFromLocalStorage(activeUserIdKey);
@ -109,9 +110,6 @@ async function loadNotificationBar() {
const userSettingsStorageValue = await getFromLocalStorage(activeUserId); const userSettingsStorageValue = await getFromLocalStorage(activeUserId);
if (userSettingsStorageValue[activeUserId]) { if (userSettingsStorageValue[activeUserId]) {
const userSettings: UserSettings = userSettingsStorageValue[activeUserId].settings; const userSettings: UserSettings = userSettingsStorageValue[activeUserId].settings;
const globalSettings: GlobalSettings = (await getFromLocalStorage(globalStorageKey))[
globalStorageKey
];
// Do not show the notification bar on the Bitwarden vault // Do not show the notification bar on the Bitwarden vault
// because they can add logins and change passwords there // because they can add logins and change passwords there
@ -122,8 +120,8 @@ async function loadNotificationBar() {
// show the notification bar on (for login detail collection or password change). // show the notification bar on (for login detail collection or password change).
// It is managed in the Settings > Excluded Domains page in the browser extension. // It is managed in the Settings > Excluded Domains page in the browser extension.
// Example: '{"bitwarden.com":null}' // Example: '{"bitwarden.com":null}'
const excludedDomainsDict = globalSettings.neverDomains;
if (!excludedDomainsDict || !(window.location.hostname in excludedDomainsDict)) { if (!excludedDomains || !(window.location.hostname in excludedDomains)) {
if (enableAddedLoginPrompt || enableChangedPasswordPrompt) { if (enableAddedLoginPrompt || enableChangedPasswordPrompt) {
// If the user has not disabled both notifications, then handle the initial page change (null -> actual page) // If the user has not disabled both notifications, then handle the initial page change (null -> actual page)
handlePageChange(); handlePageChange();

View File

@ -1,14 +1,16 @@
import { Component, OnInit } from "@angular/core"; import { Component, OnInit } from "@angular/core";
import { firstValueFrom } from "rxjs"; import { firstValueFrom } from "rxjs";
import { SettingsService } from "@bitwarden/common/abstractions/settings.service";
import { AutofillOverlayVisibility } from "@bitwarden/common/autofill/constants"; import { AutofillOverlayVisibility } from "@bitwarden/common/autofill/constants";
import { AutofillSettingsServiceAbstraction } from "@bitwarden/common/autofill/services/autofill-settings.service"; import { AutofillSettingsServiceAbstraction } from "@bitwarden/common/autofill/services/autofill-settings.service";
import { DomainSettingsService } from "@bitwarden/common/autofill/services/domain-settings.service";
import { InlineMenuVisibilitySetting } from "@bitwarden/common/autofill/types"; import { InlineMenuVisibilitySetting } from "@bitwarden/common/autofill/types";
import {
UriMatchStrategy,
UriMatchStrategySetting,
} from "@bitwarden/common/models/domain/domain-service";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
import { StateService } from "@bitwarden/common/platform/abstractions/state.service";
import { UriMatchType } from "@bitwarden/common/vault/enums";
import { DialogService } from "@bitwarden/components"; import { DialogService } from "@bitwarden/components";
import { BrowserApi } from "../../../platform/browser/browser-api"; import { BrowserApi } from "../../../platform/browser/browser-api";
@ -28,16 +30,15 @@ export class AutofillComponent implements OnInit {
enableAutoFillOnPageLoad = false; enableAutoFillOnPageLoad = false;
autoFillOnPageLoadDefault = false; autoFillOnPageLoadDefault = false;
autoFillOnPageLoadOptions: any[]; autoFillOnPageLoadOptions: any[];
defaultUriMatch = UriMatchType.Domain; defaultUriMatch: UriMatchStrategySetting = UriMatchStrategy.Domain;
uriMatchOptions: any[]; uriMatchOptions: any[];
autofillKeyboardHelperText: string; autofillKeyboardHelperText: string;
accountSwitcherEnabled = false; accountSwitcherEnabled = false;
constructor( constructor(
private stateService: StateService,
private i18nService: I18nService, private i18nService: I18nService,
private platformUtilsService: PlatformUtilsService, private platformUtilsService: PlatformUtilsService,
private settingsService: SettingsService, private domainSettingsService: DomainSettingsService,
private autofillService: AutofillService, private autofillService: AutofillService,
private dialogService: DialogService, private dialogService: DialogService,
private autofillSettingsService: AutofillSettingsServiceAbstraction, private autofillSettingsService: AutofillSettingsServiceAbstraction,
@ -61,12 +62,12 @@ export class AutofillComponent implements OnInit {
{ name: i18nService.t("autoFillOnPageLoadNo"), value: false }, { name: i18nService.t("autoFillOnPageLoadNo"), value: false },
]; ];
this.uriMatchOptions = [ this.uriMatchOptions = [
{ name: i18nService.t("baseDomain"), value: UriMatchType.Domain }, { name: i18nService.t("baseDomain"), value: UriMatchStrategy.Domain },
{ name: i18nService.t("host"), value: UriMatchType.Host }, { name: i18nService.t("host"), value: UriMatchStrategy.Host },
{ name: i18nService.t("startsWith"), value: UriMatchType.StartsWith }, { name: i18nService.t("startsWith"), value: UriMatchStrategy.StartsWith },
{ name: i18nService.t("regEx"), value: UriMatchType.RegularExpression }, { name: i18nService.t("regEx"), value: UriMatchStrategy.RegularExpression },
{ name: i18nService.t("exact"), value: UriMatchType.Exact }, { name: i18nService.t("exact"), value: UriMatchStrategy.Exact },
{ name: i18nService.t("never"), value: UriMatchType.Never }, { name: i18nService.t("never"), value: UriMatchStrategy.Never },
]; ];
this.accountSwitcherEnabled = enableAccountSwitching(); this.accountSwitcherEnabled = enableAccountSwitching();
@ -94,8 +95,10 @@ export class AutofillComponent implements OnInit {
this.autofillSettingsService.autofillOnPageLoadDefault$, this.autofillSettingsService.autofillOnPageLoadDefault$,
); );
const defaultUriMatch = await this.stateService.getDefaultUriMatch(); const defaultUriMatch = await firstValueFrom(
this.defaultUriMatch = defaultUriMatch == null ? UriMatchType.Domain : defaultUriMatch; this.domainSettingsService.defaultUriMatchStrategy$,
);
this.defaultUriMatch = defaultUriMatch == null ? UriMatchStrategy.Domain : defaultUriMatch;
const command = await this.platformUtilsService.getAutofillKeyboardShortcut(); const command = await this.platformUtilsService.getAutofillKeyboardShortcut();
await this.setAutofillKeyboardHelperText(command); await this.setAutofillKeyboardHelperText(command);
@ -119,7 +122,7 @@ export class AutofillComponent implements OnInit {
} }
async saveDefaultUriMatch() { async saveDefaultUriMatch() {
await this.stateService.setDefaultUriMatch(this.defaultUriMatch); await this.domainSettingsService.setDefaultUriMatchStrategy(this.defaultUriMatch);
} }
private async setAutofillKeyboardHelperText(command: string) { private async setAutofillKeyboardHelperText(command: string) {

View File

@ -1,4 +1,5 @@
import { UriMatchType, CipherType } from "@bitwarden/common/vault/enums"; import { UriMatchStrategySetting } from "@bitwarden/common/models/domain/domain-service";
import { CipherType } from "@bitwarden/common/vault/enums";
import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view"; import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
import AutofillField from "../../models/autofill-field"; import AutofillField from "../../models/autofill-field";
@ -40,7 +41,7 @@ export interface GenerateFillScriptOptions {
allowTotpAutofill: boolean; allowTotpAutofill: boolean;
cipher: CipherView; cipher: CipherView;
tabUrl: string; tabUrl: string;
defaultUriMatch: UriMatchType; defaultUriMatch: UriMatchStrategySetting;
} }
export abstract class AutofillService { export abstract class AutofillService {

View File

@ -1,19 +1,25 @@
import { mock, mockReset } from "jest-mock-extended"; import { mock, mockReset } from "jest-mock-extended";
import { of } from "rxjs";
import { UserVerificationService } from "@bitwarden/common/auth/services/user-verification/user-verification.service"; import { UserVerificationService } from "@bitwarden/common/auth/services/user-verification/user-verification.service";
import { AutofillOverlayVisibility } from "@bitwarden/common/autofill/constants"; import { AutofillOverlayVisibility } from "@bitwarden/common/autofill/constants";
import { AutofillSettingsService } from "@bitwarden/common/autofill/services/autofill-settings.service"; import { AutofillSettingsService } from "@bitwarden/common/autofill/services/autofill-settings.service";
import { EventType } from "@bitwarden/common/enums";
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
import { EventCollectionService } from "@bitwarden/common/services/event/event-collection.service";
import { SettingsService } from "@bitwarden/common/services/settings.service";
import { import {
FieldType, DefaultDomainSettingsService,
LinkedIdType, DomainSettingsService,
LoginLinkedId, } from "@bitwarden/common/autofill/services/domain-settings.service";
UriMatchType, import { EventType } from "@bitwarden/common/enums";
CipherType, import { UriMatchStrategy } from "@bitwarden/common/models/domain/domain-service";
} from "@bitwarden/common/vault/enums"; import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
import { Utils } from "@bitwarden/common/platform/misc/utils";
import { EventCollectionService } from "@bitwarden/common/services/event/event-collection.service";
import {
FakeStateProvider,
FakeAccountService,
mockAccountServiceWith,
} from "@bitwarden/common/spec";
import { UserId } from "@bitwarden/common/types/guid";
import { FieldType, LinkedIdType, LoginLinkedId, CipherType } from "@bitwarden/common/vault/enums";
import { CipherRepromptType } from "@bitwarden/common/vault/enums/cipher-reprompt-type"; import { CipherRepromptType } from "@bitwarden/common/vault/enums/cipher-reprompt-type";
import { CardView } from "@bitwarden/common/vault/models/view/card.view"; import { CardView } from "@bitwarden/common/vault/models/view/card.view";
import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view"; import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
@ -47,15 +53,24 @@ import {
import { AutoFillConstants, IdentityAutoFillConstants } from "./autofill-constants"; import { AutoFillConstants, IdentityAutoFillConstants } from "./autofill-constants";
import AutofillService from "./autofill.service"; import AutofillService from "./autofill.service";
const mockEquivalentDomains = [
["example.com", "exampleapp.com", "example.co.uk", "ejemplo.es"],
["bitwarden.com", "bitwarden.co.uk", "sm-bitwarden.com"],
["example.co.uk", "exampleapp.co.uk"],
];
describe("AutofillService", () => { describe("AutofillService", () => {
let autofillService: AutofillService; let autofillService: AutofillService;
const cipherService = mock<CipherService>(); const cipherService = mock<CipherService>();
const stateService = mock<BrowserStateService>(); const stateService = mock<BrowserStateService>();
const autofillSettingsService = mock<AutofillSettingsService>(); const autofillSettingsService = mock<AutofillSettingsService>();
const mockUserId = Utils.newGuid() as UserId;
const accountService: FakeAccountService = mockAccountServiceWith(mockUserId);
const fakeStateProvider: FakeStateProvider = new FakeStateProvider(accountService);
let domainSettingsService: DomainSettingsService;
const totpService = mock<TotpService>(); const totpService = mock<TotpService>();
const eventCollectionService = mock<EventCollectionService>(); const eventCollectionService = mock<EventCollectionService>();
const logService = mock<LogService>(); const logService = mock<LogService>();
const settingsService = mock<SettingsService>();
const userVerificationService = mock<UserVerificationService>(); const userVerificationService = mock<UserVerificationService>();
beforeEach(() => { beforeEach(() => {
@ -66,9 +81,12 @@ describe("AutofillService", () => {
totpService, totpService,
eventCollectionService, eventCollectionService,
logService, logService,
settingsService, domainSettingsService,
userVerificationService, userVerificationService,
); );
domainSettingsService = new DefaultDomainSettingsService(fakeStateProvider);
domainSettingsService.equivalentDomains$ = of(mockEquivalentDomains);
}); });
afterEach(() => { afterEach(() => {
@ -407,6 +425,8 @@ describe("AutofillService", () => {
autofillOptions.cipher.login.matchesUri = jest.fn().mockReturnValue(true); autofillOptions.cipher.login.matchesUri = jest.fn().mockReturnValue(true);
autofillOptions.cipher.login.username = "username"; autofillOptions.cipher.login.username = "username";
autofillOptions.cipher.login.password = "password"; autofillOptions.cipher.login.password = "password";
jest.spyOn(autofillService, "getDefaultUriMatchStrategy").mockResolvedValue(0);
}); });
describe("given a set of autofill options that are incomplete", () => { describe("given a set of autofill options that are incomplete", () => {
@ -468,7 +488,6 @@ describe("AutofillService", () => {
it("will autofill login data for a page", async () => { it("will autofill login data for a page", async () => {
jest.spyOn(stateService, "getCanAccessPremium"); jest.spyOn(stateService, "getCanAccessPremium");
jest.spyOn(stateService, "getDefaultUriMatch");
jest.spyOn(autofillService as any, "generateFillScript"); jest.spyOn(autofillService as any, "generateFillScript");
jest.spyOn(autofillService as any, "generateLoginFillScript"); jest.spyOn(autofillService as any, "generateLoginFillScript");
jest.spyOn(logService, "info"); jest.spyOn(logService, "info");
@ -479,7 +498,7 @@ describe("AutofillService", () => {
const currentAutofillPageDetails = autofillOptions.pageDetails[0]; const currentAutofillPageDetails = autofillOptions.pageDetails[0];
expect(stateService.getCanAccessPremium).toHaveBeenCalled(); expect(stateService.getCanAccessPremium).toHaveBeenCalled();
expect(stateService.getDefaultUriMatch).toHaveBeenCalled(); expect(autofillService["getDefaultUriMatchStrategy"]).toHaveBeenCalled();
expect(autofillService["generateFillScript"]).toHaveBeenCalledWith( expect(autofillService["generateFillScript"]).toHaveBeenCalledWith(
currentAutofillPageDetails.details, currentAutofillPageDetails.details,
{ {
@ -1488,7 +1507,7 @@ describe("AutofillService", () => {
}; };
defaultLoginUriView = mock<LoginUriView>({ defaultLoginUriView = mock<LoginUriView>({
uri: "https://www.example.com", uri: "https://www.example.com",
match: UriMatchType.Domain, match: UriMatchStrategy.Domain,
}); });
options = createGenerateFillScriptOptionsMock(); options = createGenerateFillScriptOptionsMock();
options.cipher.login = mock<LoginView>({ options.cipher.login = mock<LoginView>({
@ -1559,13 +1578,13 @@ describe("AutofillService", () => {
]); ]);
}); });
it("skips adding any login uri views that have a UriMatchType of Never to the list of saved urls", async () => { it("skips adding any login uri views that have a UriMatchStrategySetting of Never to the list of saved urls", async () => {
const secondUriView = mock<LoginUriView>({ const secondUriView = mock<LoginUriView>({
uri: "https://www.second-example.com", uri: "https://www.second-example.com",
}); });
const thirdUriView = mock<LoginUriView>({ const thirdUriView = mock<LoginUriView>({
uri: "https://www.third-example.com", uri: "https://www.third-example.com",
match: UriMatchType.Never, match: UriMatchStrategy.Never,
}); });
options.cipher.login.uris = [defaultLoginUriView, secondUriView, thirdUriView]; options.cipher.login.uris = [defaultLoginUriView, secondUriView, thirdUriView];
@ -2752,31 +2771,32 @@ describe("AutofillService", () => {
}); });
describe("inUntrustedIframe", () => { describe("inUntrustedIframe", () => {
it("returns a false value if the passed pageUrl is equal to the options tabUrl", () => { it("returns a false value if the passed pageUrl is equal to the options tabUrl", async () => {
const pageUrl = "https://www.example.com"; const pageUrl = "https://www.example.com";
const tabUrl = "https://www.example.com"; const tabUrl = "https://www.example.com";
const generateFillScriptOptions = createGenerateFillScriptOptionsMock({ tabUrl }); const generateFillScriptOptions = createGenerateFillScriptOptionsMock({ tabUrl });
generateFillScriptOptions.cipher.login.matchesUri = jest.fn().mockReturnValueOnce(true); generateFillScriptOptions.cipher.login.matchesUri = jest.fn().mockReturnValueOnce(true);
jest.spyOn(settingsService, "getEquivalentDomains");
const result = autofillService["inUntrustedIframe"](pageUrl, generateFillScriptOptions); const result = await autofillService["inUntrustedIframe"](pageUrl, generateFillScriptOptions);
expect(settingsService.getEquivalentDomains).not.toHaveBeenCalled();
expect(generateFillScriptOptions.cipher.login.matchesUri).not.toHaveBeenCalled(); expect(generateFillScriptOptions.cipher.login.matchesUri).not.toHaveBeenCalled();
expect(result).toBe(false); expect(result).toBe(false);
}); });
it("returns a false value if the passed pageUrl matches the domain of the tabUrl", () => { it("returns a false value if the passed pageUrl matches the domain of the tabUrl", async () => {
const pageUrl = "https://subdomain.example.com"; const pageUrl = "https://subdomain.example.com";
const tabUrl = "https://www.example.com"; const tabUrl = "https://www.example.com";
const equivalentDomains = new Set(["example.com"]); const equivalentDomains = new Set([
"ejemplo.es",
"example.co.uk",
"example.com",
"exampleapp.com",
]);
const generateFillScriptOptions = createGenerateFillScriptOptionsMock({ tabUrl }); const generateFillScriptOptions = createGenerateFillScriptOptionsMock({ tabUrl });
generateFillScriptOptions.cipher.login.matchesUri = jest.fn().mockReturnValueOnce(true); generateFillScriptOptions.cipher.login.matchesUri = jest.fn().mockReturnValueOnce(true);
jest.spyOn(settingsService as any, "getEquivalentDomains").mockReturnValue(equivalentDomains);
const result = autofillService["inUntrustedIframe"](pageUrl, generateFillScriptOptions); const result = await autofillService["inUntrustedIframe"](pageUrl, generateFillScriptOptions);
expect(settingsService.getEquivalentDomains).toHaveBeenCalledWith(pageUrl);
expect(generateFillScriptOptions.cipher.login.matchesUri).toHaveBeenCalledWith( expect(generateFillScriptOptions.cipher.login.matchesUri).toHaveBeenCalledWith(
pageUrl, pageUrl,
equivalentDomains, equivalentDomains,
@ -2785,17 +2805,21 @@ describe("AutofillService", () => {
expect(result).toBe(false); expect(result).toBe(false);
}); });
it("returns a true value if the passed pageUrl does not match the domain of the tabUrl", () => { it("returns a true value if the passed pageUrl does not match the domain of the tabUrl", async () => {
const equivalentDomains = new Set([
"ejemplo.es",
"example.co.uk",
"example.com",
"exampleapp.com",
]);
domainSettingsService.equivalentDomains$ = of([["not-example.com"]]);
const pageUrl = "https://subdomain.example.com"; const pageUrl = "https://subdomain.example.com";
const tabUrl = "https://www.not-example.com"; const tabUrl = "https://www.not-example.com";
const equivalentDomains = new Set(["not-example.com"]);
const generateFillScriptOptions = createGenerateFillScriptOptionsMock({ tabUrl }); const generateFillScriptOptions = createGenerateFillScriptOptionsMock({ tabUrl });
generateFillScriptOptions.cipher.login.matchesUri = jest.fn().mockReturnValueOnce(false); generateFillScriptOptions.cipher.login.matchesUri = jest.fn().mockReturnValueOnce(false);
jest.spyOn(settingsService as any, "getEquivalentDomains").mockReturnValue(equivalentDomains);
const result = autofillService["inUntrustedIframe"](pageUrl, generateFillScriptOptions); const result = await autofillService["inUntrustedIframe"](pageUrl, generateFillScriptOptions);
expect(settingsService.getEquivalentDomains).toHaveBeenCalledWith(pageUrl);
expect(generateFillScriptOptions.cipher.login.matchesUri).toHaveBeenCalledWith( expect(generateFillScriptOptions.cipher.login.matchesUri).toHaveBeenCalledWith(
pageUrl, pageUrl,
equivalentDomains, equivalentDomains,

View File

@ -1,15 +1,19 @@
import { firstValueFrom } from "rxjs"; import { firstValueFrom } from "rxjs";
import { EventCollectionService } from "@bitwarden/common/abstractions/event/event-collection.service"; import { EventCollectionService } from "@bitwarden/common/abstractions/event/event-collection.service";
import { SettingsService } from "@bitwarden/common/abstractions/settings.service";
import { UserVerificationService } from "@bitwarden/common/auth/abstractions/user-verification/user-verification.service.abstraction"; import { UserVerificationService } from "@bitwarden/common/auth/abstractions/user-verification/user-verification.service.abstraction";
import { AutofillSettingsServiceAbstraction } from "@bitwarden/common/autofill/services/autofill-settings.service"; import { AutofillSettingsServiceAbstraction } from "@bitwarden/common/autofill/services/autofill-settings.service";
import { DomainSettingsService } from "@bitwarden/common/autofill/services/domain-settings.service";
import { InlineMenuVisibilitySetting } from "@bitwarden/common/autofill/types"; import { InlineMenuVisibilitySetting } from "@bitwarden/common/autofill/types";
import { EventType } from "@bitwarden/common/enums"; import { EventType } from "@bitwarden/common/enums";
import {
UriMatchStrategySetting,
UriMatchStrategy,
} from "@bitwarden/common/models/domain/domain-service";
import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service"; import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
import { TotpService } from "@bitwarden/common/vault/abstractions/totp.service"; import { TotpService } from "@bitwarden/common/vault/abstractions/totp.service";
import { FieldType, UriMatchType, CipherType } from "@bitwarden/common/vault/enums"; import { FieldType, CipherType } from "@bitwarden/common/vault/enums";
import { CipherRepromptType } from "@bitwarden/common/vault/enums/cipher-reprompt-type"; import { CipherRepromptType } from "@bitwarden/common/vault/enums/cipher-reprompt-type";
import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view"; import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
import { FieldView } from "@bitwarden/common/vault/models/view/field.view"; import { FieldView } from "@bitwarden/common/vault/models/view/field.view";
@ -48,7 +52,7 @@ export default class AutofillService implements AutofillServiceInterface {
private totpService: TotpService, private totpService: TotpService,
private eventCollectionService: EventCollectionService, private eventCollectionService: EventCollectionService,
private logService: LogService, private logService: LogService,
private settingsService: SettingsService, private domainSettingsService: DomainSettingsService,
private userVerificationService: UserVerificationService, private userVerificationService: UserVerificationService,
) {} ) {}
@ -215,6 +219,13 @@ export default class AutofillService implements AutofillServiceInterface {
return await firstValueFrom(this.autofillSettingsService.autofillOnPageLoad$); return await firstValueFrom(this.autofillSettingsService.autofillOnPageLoad$);
} }
/**
* Gets the default URI match strategy setting from the domain settings service.
*/
async getDefaultUriMatchStrategy(): Promise<UriMatchStrategySetting> {
return await firstValueFrom(this.domainSettingsService.defaultUriMatchStrategy$);
}
/** /**
* Autofill a given tab with a given login item * Autofill a given tab with a given login item
* @param {AutoFillOptions} options Instructions about the autofill operation, including tab and login item * @param {AutoFillOptions} options Instructions about the autofill operation, including tab and login item
@ -229,7 +240,7 @@ export default class AutofillService implements AutofillServiceInterface {
let totp: string | null = null; let totp: string | null = null;
const canAccessPremium = await this.stateService.getCanAccessPremium(); const canAccessPremium = await this.stateService.getCanAccessPremium();
const defaultUriMatch = (await this.stateService.getDefaultUriMatch()) ?? UriMatchType.Domain; const defaultUriMatch = await this.getDefaultUriMatchStrategy();
if (!canAccessPremium) { if (!canAccessPremium) {
options.cipher.login.totp = null; options.cipher.login.totp = null;
@ -579,9 +590,9 @@ export default class AutofillService implements AutofillServiceInterface {
let totp: AutofillField = null; let totp: AutofillField = null;
const login = options.cipher.login; const login = options.cipher.login;
fillScript.savedUrls = fillScript.savedUrls =
login?.uris?.filter((u) => u.match != UriMatchType.Never).map((u) => u.uri) ?? []; login?.uris?.filter((u) => u.match != UriMatchStrategy.Never).map((u) => u.uri) ?? [];
fillScript.untrustedIframe = this.inUntrustedIframe(pageDetails.url, options); fillScript.untrustedIframe = await this.inUntrustedIframe(pageDetails.url, options);
let passwordFields = AutofillService.loadPasswordFields( let passwordFields = AutofillService.loadPasswordFields(
pageDetails, pageDetails,
@ -1066,7 +1077,10 @@ export default class AutofillService implements AutofillServiceInterface {
* @returns {boolean} `true` if the iframe is untrusted and a warning should be shown, `false` otherwise * @returns {boolean} `true` if the iframe is untrusted and a warning should be shown, `false` otherwise
* @private * @private
*/ */
private inUntrustedIframe(pageUrl: string, options: GenerateFillScriptOptions): boolean { private async inUntrustedIframe(
pageUrl: string,
options: GenerateFillScriptOptions,
): Promise<boolean> {
// If the pageUrl (from the content script) matches the tabUrl (from the sender tab), we are not in an iframe // If the pageUrl (from the content script) matches the tabUrl (from the sender tab), we are not in an iframe
// This also avoids a false positive if no URI is saved and the user triggers auto-fill anyway // This also avoids a false positive if no URI is saved and the user triggers auto-fill anyway
if (pageUrl === options.tabUrl) { if (pageUrl === options.tabUrl) {
@ -1076,7 +1090,9 @@ export default class AutofillService implements AutofillServiceInterface {
// Check the pageUrl against cipher URIs using the configured match detection. // Check the pageUrl against cipher URIs using the configured match detection.
// Remember: if we are in this function, the tabUrl already matches a saved URI for the login. // Remember: if we are in this function, the tabUrl already matches a saved URI for the login.
// We need to verify the pageUrl also matches. // We need to verify the pageUrl also matches.
const equivalentDomains = this.settingsService.getEquivalentDomains(pageUrl); const equivalentDomains = await firstValueFrom(
this.domainSettingsService.getUrlEquivalentDomains(pageUrl),
);
const matchesUri = options.cipher.login.matchesUri( const matchesUri = options.cipher.login.matchesUri(
pageUrl, pageUrl,
equivalentDomains, equivalentDomains,

View File

@ -1,8 +1,9 @@
import { mock } from "jest-mock-extended"; import { mock } from "jest-mock-extended";
import { AuthenticationStatus } from "@bitwarden/common/auth/enums/authentication-status"; import { AuthenticationStatus } from "@bitwarden/common/auth/enums/authentication-status";
import { UriMatchStrategy } from "@bitwarden/common/models/domain/domain-service";
import { ThemeType } from "@bitwarden/common/platform/enums"; import { ThemeType } from "@bitwarden/common/platform/enums";
import { UriMatchType, CipherType } from "@bitwarden/common/vault/enums"; import { CipherType } from "@bitwarden/common/vault/enums";
import { CipherRepromptType } from "@bitwarden/common/vault/enums/cipher-reprompt-type"; import { CipherRepromptType } from "@bitwarden/common/vault/enums/cipher-reprompt-type";
import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view"; import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
@ -112,7 +113,7 @@ function createGenerateFillScriptOptionsMock(customFields = {}): GenerateFillScr
allowTotpAutofill: false, allowTotpAutofill: false,
cipher: mock<CipherView>(), cipher: mock<CipherView>(),
tabUrl: "https://jest-testing-website.com", tabUrl: "https://jest-testing-website.com",
defaultUriMatch: UriMatchType.Domain, defaultUriMatch: UriMatchStrategy.Domain,
...customFields, ...customFields,
}; };
} }

View File

@ -1,5 +1,4 @@
import { Region } from "@bitwarden/common/platform/abstractions/environment.service"; import { Region } from "@bitwarden/common/platform/abstractions/environment.service";
import { GlobalState } from "@bitwarden/common/platform/models/domain/global-state";
import { VaultTimeoutAction } from "@bitwarden/common/src/enums/vault-timeout-action.enum"; import { VaultTimeoutAction } from "@bitwarden/common/src/enums/vault-timeout-action.enum";
import { CipherType } from "@bitwarden/common/vault/enums"; import { CipherType } from "@bitwarden/common/vault/enums";
@ -32,15 +31,10 @@ export type UserSettings = {
utcDate: string; utcDate: string;
version: string; version: string;
}; };
settings: {
equivalentDomains: string[][];
};
vaultTimeout: number; vaultTimeout: number;
vaultTimeoutAction: VaultTimeoutAction; vaultTimeoutAction: VaultTimeoutAction;
}; };
export type GlobalSettings = Pick<GlobalState, "neverDomains">;
/** /**
* A HTMLElement (usually a form element) with additional custom properties added by this script * A HTMLElement (usually a form element) with additional custom properties added by this script
*/ */

View File

@ -56,6 +56,10 @@ import {
BadgeSettingsServiceAbstraction, BadgeSettingsServiceAbstraction,
BadgeSettingsService, BadgeSettingsService,
} from "@bitwarden/common/autofill/services/badge-settings.service"; } from "@bitwarden/common/autofill/services/badge-settings.service";
import {
DomainSettingsService,
DefaultDomainSettingsService,
} from "@bitwarden/common/autofill/services/domain-settings.service";
import { import {
UserNotificationSettingsService, UserNotificationSettingsService,
UserNotificationSettingsServiceAbstraction, UserNotificationSettingsServiceAbstraction,
@ -198,10 +202,11 @@ import { BrowserEnvironmentService } from "../platform/services/browser-environm
import BrowserLocalStorageService from "../platform/services/browser-local-storage.service"; import BrowserLocalStorageService from "../platform/services/browser-local-storage.service";
import BrowserMessagingPrivateModeBackgroundService from "../platform/services/browser-messaging-private-mode-background.service"; import BrowserMessagingPrivateModeBackgroundService from "../platform/services/browser-messaging-private-mode-background.service";
import BrowserMessagingService from "../platform/services/browser-messaging.service"; import BrowserMessagingService from "../platform/services/browser-messaging.service";
import BrowserPlatformUtilsService from "../platform/services/browser-platform-utils.service";
import { BrowserStateService } from "../platform/services/browser-state.service"; import { BrowserStateService } from "../platform/services/browser-state.service";
import I18nService from "../platform/services/i18n.service"; import I18nService from "../platform/services/i18n.service";
import { LocalBackedSessionStorageService } from "../platform/services/local-backed-session-storage.service"; import { LocalBackedSessionStorageService } from "../platform/services/local-backed-session-storage.service";
import { BackgroundPlatformUtilsService } from "../platform/services/platform-utils/background-platform-utils.service";
import { BrowserPlatformUtilsService } from "../platform/services/platform-utils/browser-platform-utils.service";
import { BackgroundDerivedStateProvider } from "../platform/state/background-derived-state.provider"; import { BackgroundDerivedStateProvider } from "../platform/state/background-derived-state.provider";
import { BackgroundMemoryStorageService } from "../platform/storage/background-memory-storage.service"; import { BackgroundMemoryStorageService } from "../platform/storage/background-memory-storage.service";
import { BrowserSendService } from "../services/browser-send.service"; import { BrowserSendService } from "../services/browser-send.service";
@ -258,6 +263,7 @@ export default class MainBackground {
userNotificationSettingsService: UserNotificationSettingsServiceAbstraction; userNotificationSettingsService: UserNotificationSettingsServiceAbstraction;
autofillSettingsService: AutofillSettingsServiceAbstraction; autofillSettingsService: AutofillSettingsServiceAbstraction;
badgeSettingsService: BadgeSettingsServiceAbstraction; badgeSettingsService: BadgeSettingsServiceAbstraction;
domainSettingsService: DomainSettingsService;
systemService: SystemServiceAbstraction; systemService: SystemServiceAbstraction;
eventCollectionService: EventCollectionServiceAbstraction; eventCollectionService: EventCollectionServiceAbstraction;
eventUploadService: EventUploadServiceAbstraction; eventUploadService: EventUploadServiceAbstraction;
@ -438,28 +444,10 @@ export default class MainBackground {
migrationRunner, migrationRunner,
); );
this.userNotificationSettingsService = new UserNotificationSettingsService(this.stateProvider); this.userNotificationSettingsService = new UserNotificationSettingsService(this.stateProvider);
this.platformUtilsService = new BrowserPlatformUtilsService( this.platformUtilsService = new BackgroundPlatformUtilsService(
this.messagingService, this.messagingService,
(clipboardValue, clearMs) => { (clipboardValue, clearMs) => this.clearClipboard(clipboardValue, clearMs),
if (this.systemService != null) { async () => this.biometricUnlock(),
// 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.systemService.clearClipboard(clipboardValue, clearMs);
}
},
async () => {
if (this.nativeMessagingBackground != null) {
const promise = this.nativeMessagingBackground.getResponse();
try {
await this.nativeMessagingBackground.send({ command: "biometricUnlock" });
} catch (e) {
return Promise.reject(e);
}
return promise.then((result) => result.response === "unlocked");
}
},
self, self,
); );
this.i18nService = new I18nService(BrowserApi.getUILanguage(), this.globalStateProvider); this.i18nService = new I18nService(BrowserApi.getUILanguage(), this.globalStateProvider);
@ -475,7 +463,7 @@ export default class MainBackground {
this.biometricStateService, this.biometricStateService,
); );
this.tokenService = new TokenService(this.stateService); this.tokenService = new TokenService(this.stateService);
this.appIdService = new AppIdService(this.storageService); this.appIdService = new AppIdService(this.globalStateProvider);
this.apiService = new ApiService( this.apiService = new ApiService(
this.tokenService, this.tokenService,
this.platformUtilsService, this.platformUtilsService,
@ -483,6 +471,7 @@ export default class MainBackground {
this.appIdService, this.appIdService,
(expired: boolean) => this.logout(expired), (expired: boolean) => this.logout(expired),
); );
this.domainSettingsService = new DefaultDomainSettingsService(this.stateProvider);
this.settingsService = new BrowserSettingsService(this.stateService); this.settingsService = new BrowserSettingsService(this.stateService);
this.fileUploadService = new FileUploadService(this.logService); this.fileUploadService = new FileUploadService(this.logService);
this.cipherFileUploadService = new CipherFileUploadService( this.cipherFileUploadService = new CipherFileUploadService(
@ -588,6 +577,7 @@ export default class MainBackground {
this.policyService, this.policyService,
this.deviceTrustCryptoService, this.deviceTrustCryptoService,
this.authRequestService, this.authRequestService,
this.globalStateProvider,
); );
this.ssoLoginService = new SsoLoginService(this.stateProvider); this.ssoLoginService = new SsoLoginService(this.stateProvider);
@ -607,7 +597,7 @@ export default class MainBackground {
this.cipherService = new CipherService( this.cipherService = new CipherService(
this.cryptoService, this.cryptoService,
this.settingsService, this.domainSettingsService,
this.apiService, this.apiService,
this.i18nService, this.i18nService,
this.searchService, this.searchService,
@ -695,7 +685,7 @@ export default class MainBackground {
this.providerService = new ProviderService(this.stateProvider); this.providerService = new ProviderService(this.stateProvider);
this.syncService = new SyncService( this.syncService = new SyncService(
this.apiService, this.apiService,
this.settingsService, this.domainSettingsService,
this.folderService, this.folderService,
this.cipherService, this.cipherService,
this.cryptoService, this.cryptoService,
@ -732,7 +722,7 @@ export default class MainBackground {
this.totpService, this.totpService,
this.eventCollectionService, this.eventCollectionService,
this.logService, this.logService,
this.settingsService, this.domainSettingsService,
this.userVerificationService, this.userVerificationService,
); );
this.auditService = new AuditService(this.cryptoFunctionService, this.apiService); this.auditService = new AuditService(this.cryptoFunctionService, this.apiService);
@ -796,6 +786,7 @@ export default class MainBackground {
this.authService, this.authService,
this.stateService, this.stateService,
this.vaultSettingsService, this.vaultSettingsService,
this.domainSettingsService,
this.logService, this.logService,
); );
@ -865,6 +856,7 @@ export default class MainBackground {
this.folderService, this.folderService,
this.stateService, this.stateService,
this.userNotificationSettingsService, this.userNotificationSettingsService,
this.domainSettingsService,
this.environmentService, this.environmentService,
this.logService, this.logService,
); );
@ -1098,7 +1090,6 @@ export default class MainBackground {
await Promise.all([ await Promise.all([
this.syncService.setLastSync(new Date(0), userId), this.syncService.setLastSync(new Date(0), userId),
this.cryptoService.clearKeys(userId), this.cryptoService.clearKeys(userId),
this.settingsService.clear(userId),
this.cipherService.clear(userId), this.cipherService.clear(userId),
this.folderService.clear(userId), this.folderService.clear(userId),
this.collectionService.clear(userId), this.collectionService.clear(userId),
@ -1223,6 +1214,23 @@ export default class MainBackground {
} }
} }
async clearClipboard(clipboardValue: string, clearMs: number) {
if (this.systemService != null) {
await this.systemService.clearClipboard(clipboardValue, clearMs);
}
}
async biometricUnlock(): Promise<boolean> {
if (this.nativeMessagingBackground == null) {
return false;
}
const responsePromise = this.nativeMessagingBackground.getResponse();
await this.nativeMessagingBackground.send({ command: "biometricUnlock" });
const response = await responsePromise;
return response.response === "unlocked";
}
private async fullSync(override = false) { private async fullSync(override = false) {
const syncInternal = 6 * 60 * 60 * 1000; // 6 hours const syncInternal = 6 * 60 * 60 * 1000; // 6 hours
const lastSync = await this.syncService.getLastSync(); const lastSync = await this.syncService.getLastSync();

View File

@ -122,7 +122,7 @@ export class NativeMessagingBackground {
break; break;
case "disconnected": case "disconnected":
if (this.connecting) { if (this.connecting) {
reject("startDesktop"); reject(new Error("startDesktop"));
} }
this.connected = false; this.connected = false;
this.port.disconnect(); this.port.disconnect();
@ -203,7 +203,7 @@ export class NativeMessagingBackground {
this.connected = false; this.connected = false;
const reason = error != null ? "desktopIntegrationDisabled" : null; const reason = error != null ? "desktopIntegrationDisabled" : null;
reject(reason); reject(new Error(reason));
}); });
}); });
} }

View File

@ -19,7 +19,7 @@ import { AutofillService } from "../autofill/services/abstractions/autofill.serv
import { BrowserApi } from "../platform/browser/browser-api"; import { BrowserApi } from "../platform/browser/browser-api";
import { BrowserStateService } from "../platform/services/abstractions/browser-state.service"; import { BrowserStateService } from "../platform/services/abstractions/browser-state.service";
import { BrowserEnvironmentService } from "../platform/services/browser-environment.service"; import { BrowserEnvironmentService } from "../platform/services/browser-environment.service";
import BrowserPlatformUtilsService from "../platform/services/browser-platform-utils.service"; import { BrowserPlatformUtilsService } from "../platform/services/platform-utils/browser-platform-utils.service";
import { AbortManager } from "../vault/background/abort-manager"; import { AbortManager } from "../vault/background/abort-manager";
import { Fido2Service } from "../vault/services/abstractions/fido2.service"; import { Fido2Service } from "../vault/services/abstractions/fido2.service";
@ -68,6 +68,7 @@ export default class RuntimeBackground {
"checkFido2FeatureEnabled", "checkFido2FeatureEnabled",
"fido2RegisterCredentialRequest", "fido2RegisterCredentialRequest",
"fido2GetCredentialRequest", "fido2GetCredentialRequest",
"biometricUnlock",
]; ];
if (messagesWithResponse.includes(msg.command)) { if (messagesWithResponse.includes(msg.command)) {
@ -305,6 +306,14 @@ export default class RuntimeBackground {
); );
case "switchAccount": { case "switchAccount": {
await this.main.switchAccount(msg.userId); await this.main.switchAccount(msg.userId);
break;
}
case "clearClipboard": {
await this.main.clearClipboard(msg.clipboardValue, msg.timeoutMs);
break;
}
case "biometricUnlock": {
return await this.main.biometricUnlock();
} }
} }
} }

View File

@ -1,14 +1,15 @@
import { DiskStorageOptions } from "@koa/multer";
import { AppIdService as AbstractAppIdService } from "@bitwarden/common/platform/abstractions/app-id.service"; import { AppIdService as AbstractAppIdService } from "@bitwarden/common/platform/abstractions/app-id.service";
import { AppIdService } from "@bitwarden/common/platform/services/app-id.service"; import { AppIdService } from "@bitwarden/common/platform/services/app-id.service";
import { FactoryOptions, CachedServices, factory } from "./factory-options"; import { FactoryOptions, CachedServices, factory } from "./factory-options";
import { diskStorageServiceFactory } from "./storage-service.factory"; import {
GlobalStateProviderInitOptions,
globalStateProviderFactory,
} from "./global-state-provider.factory";
type AppIdServiceFactoryOptions = FactoryOptions; type AppIdServiceFactoryOptions = FactoryOptions;
export type AppIdServiceInitOptions = AppIdServiceFactoryOptions & DiskStorageOptions; export type AppIdServiceInitOptions = AppIdServiceFactoryOptions & GlobalStateProviderInitOptions;
export function appIdServiceFactory( export function appIdServiceFactory(
cache: { appIdService?: AbstractAppIdService } & CachedServices, cache: { appIdService?: AbstractAppIdService } & CachedServices,
@ -18,6 +19,6 @@ export function appIdServiceFactory(
cache, cache,
"appIdService", "appIdService",
opts, opts,
async () => new AppIdService(await diskStorageServiceFactory(cache, opts)), async () => new AppIdService(await globalStateProviderFactory(cache, opts)),
); );
} }

View File

@ -1,6 +1,6 @@
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
import BrowserPlatformUtilsService from "../../services/browser-platform-utils.service"; import { BackgroundPlatformUtilsService } from "../../services/platform-utils/background-platform-utils.service";
import { CachedServices, factory, FactoryOptions } from "./factory-options"; import { CachedServices, factory, FactoryOptions } from "./factory-options";
import { MessagingServiceInitOptions, messagingServiceFactory } from "./messaging-service.factory"; import { MessagingServiceInitOptions, messagingServiceFactory } from "./messaging-service.factory";
@ -25,7 +25,7 @@ export function platformUtilsServiceFactory(
"platformUtilsService", "platformUtilsService",
opts, opts,
async () => async () =>
new BrowserPlatformUtilsService( new BackgroundPlatformUtilsService(
await messagingServiceFactory(cache, opts), await messagingServiceFactory(cache, opts),
opts.platformUtilsServiceOptions.clipboardWriteCallback, opts.platformUtilsServiceOptions.clipboardWriteCallback,
opts.platformUtilsServiceOptions.biometricCallback, opts.platformUtilsServiceOptions.biometricCallback,

View File

@ -3,7 +3,7 @@ import { Observable } from "rxjs";
import { DeviceType } from "@bitwarden/common/enums"; import { DeviceType } from "@bitwarden/common/enums";
import { TabMessage } from "../../types/tab-messages"; import { TabMessage } from "../../types/tab-messages";
import BrowserPlatformUtilsService from "../services/browser-platform-utils.service"; import { BrowserPlatformUtilsService } from "../services/platform-utils/browser-platform-utils.service";
export class BrowserApi { export class BrowserApi {
static isWebExtensionsApi: boolean = typeof browser !== "undefined"; static isWebExtensionsApi: boolean = typeof browser !== "undefined";

View File

@ -17,7 +17,7 @@ import { Account } from "../../models/account";
import IconDetails from "../../vault/background/models/icon-details"; import IconDetails from "../../vault/background/models/icon-details";
import { cipherServiceFactory } from "../../vault/background/service_factories/cipher-service.factory"; import { cipherServiceFactory } from "../../vault/background/service_factories/cipher-service.factory";
import { BrowserApi } from "../browser/browser-api"; import { BrowserApi } from "../browser/browser-api";
import BrowserPlatformUtilsService from "../services/browser-platform-utils.service"; import { BrowserPlatformUtilsService } from "../services/platform-utils/browser-platform-utils.service";
export type BadgeOptions = { export type BadgeOptions = {
tab?: chrome.tabs.Tab; tab?: chrome.tabs.Tab;

View File

@ -0,0 +1,28 @@
import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service";
import { BrowserPlatformUtilsService } from "./browser-platform-utils.service";
export class BackgroundPlatformUtilsService extends BrowserPlatformUtilsService {
constructor(
private messagingService: MessagingService,
clipboardWriteCallback: (clipboardValue: string, clearMs: number) => void,
biometricCallback: () => Promise<boolean>,
win: Window & typeof globalThis,
) {
super(clipboardWriteCallback, biometricCallback, win);
}
override showToast(
type: "error" | "success" | "warning" | "info",
title: string,
text: string | string[],
options?: any,
): void {
this.messagingService.send("showToast", {
text: text,
title: title,
type: type,
options: options,
});
}
}

View File

@ -1,11 +1,26 @@
import { DeviceType } from "@bitwarden/common/enums"; import { DeviceType } from "@bitwarden/common/enums";
import { flushPromises } from "../../autofill/spec/testing-utils"; import { flushPromises } from "../../../autofill/spec/testing-utils";
import { SafariApp } from "../../browser/safariApp"; import { SafariApp } from "../../../browser/safariApp";
import { BrowserApi } from "../browser/browser-api"; import { BrowserApi } from "../../browser/browser-api";
import BrowserClipboardService from "../browser-clipboard.service";
import BrowserClipboardService from "./browser-clipboard.service"; import { BrowserPlatformUtilsService } from "./browser-platform-utils.service";
import BrowserPlatformUtilsService from "./browser-platform-utils.service";
class TestBrowserPlatformUtilsService extends BrowserPlatformUtilsService {
constructor(clipboardSpy: jest.Mock, win: Window & typeof globalThis) {
super(clipboardSpy, null, win);
}
showToast(
type: "error" | "success" | "warning" | "info",
title: string,
text: string | string[],
options?: any,
): void {
throw new Error("Method not implemented.");
}
}
describe("Browser Utils Service", () => { describe("Browser Utils Service", () => {
let browserPlatformUtilsService: BrowserPlatformUtilsService; let browserPlatformUtilsService: BrowserPlatformUtilsService;
@ -13,10 +28,8 @@ describe("Browser Utils Service", () => {
beforeEach(() => { beforeEach(() => {
(window as any).matchMedia = jest.fn().mockReturnValueOnce({}); (window as any).matchMedia = jest.fn().mockReturnValueOnce({});
browserPlatformUtilsService = new BrowserPlatformUtilsService( browserPlatformUtilsService = new TestBrowserPlatformUtilsService(
null,
clipboardWriteCallbackSpy, clipboardWriteCallbackSpy,
null,
window, window,
); );
}); });

View File

@ -1,20 +1,17 @@
import { ClientType, DeviceType } from "@bitwarden/common/enums"; import { ClientType, DeviceType } from "@bitwarden/common/enums";
import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service";
import { import {
ClipboardOptions, ClipboardOptions,
PlatformUtilsService, PlatformUtilsService,
} from "@bitwarden/common/platform/abstractions/platform-utils.service"; } from "@bitwarden/common/platform/abstractions/platform-utils.service";
import { SafariApp } from "../../browser/safariApp"; import { SafariApp } from "../../../browser/safariApp";
import { BrowserApi } from "../browser/browser-api"; import { BrowserApi } from "../../browser/browser-api";
import BrowserClipboardService from "../browser-clipboard.service";
import BrowserClipboardService from "./browser-clipboard.service"; export abstract class BrowserPlatformUtilsService implements PlatformUtilsService {
export default class BrowserPlatformUtilsService implements PlatformUtilsService {
private static deviceCache: DeviceType = null; private static deviceCache: DeviceType = null;
constructor( constructor(
private messagingService: MessagingService,
private clipboardWriteCallback: (clipboardValue: string, clearMs: number) => void, private clipboardWriteCallback: (clipboardValue: string, clearMs: number) => void,
private biometricCallback: () => Promise<boolean>, private biometricCallback: () => Promise<boolean>,
private globalContext: Window | ServiceWorkerGlobalScope, private globalContext: Window | ServiceWorkerGlobalScope,
@ -193,19 +190,12 @@ export default class BrowserPlatformUtilsService implements PlatformUtilsService
return true; return true;
} }
showToast( abstract showToast(
type: "error" | "success" | "warning" | "info", type: "error" | "success" | "warning" | "info",
title: string, title: string,
text: string | string[], text: string | string[],
options?: any, options?: any,
): void { ): void;
this.messagingService.send("showToast", {
text: text,
title: title,
type: type,
options: options,
});
}
isDev(): boolean { isDev(): boolean {
return process.env.ENV === "development"; return process.env.ENV === "development";
@ -279,11 +269,10 @@ export default class BrowserPlatformUtilsService implements PlatformUtilsService
async supportsBiometric() { async supportsBiometric() {
const platformInfo = await BrowserApi.getPlatformInfo(); const platformInfo = await BrowserApi.getPlatformInfo();
if (platformInfo.os === "android") { if (platformInfo.os === "mac" || platformInfo.os === "win") {
return false; return true;
} }
return false;
return true;
} }
authenticateBiometric() { authenticateBiometric() {

View File

@ -0,0 +1,40 @@
import { SecurityContext } from "@angular/core";
import { DomSanitizer } from "@angular/platform-browser";
import { ToastrService } from "ngx-toastr";
import { BrowserPlatformUtilsService } from "./browser-platform-utils.service";
export class ForegroundPlatformUtilsService extends BrowserPlatformUtilsService {
constructor(
private sanitizer: DomSanitizer,
private toastrService: ToastrService,
clipboardWriteCallback: (clipboardValue: string, clearMs: number) => void,
biometricCallback: () => Promise<boolean>,
win: Window & typeof globalThis,
) {
super(clipboardWriteCallback, biometricCallback, win);
}
override showToast(
type: "error" | "success" | "warning" | "info",
title: string,
text: string | string[],
options?: any,
): void {
if (typeof text === "string") {
// Already in the correct format
} else if (text.length === 1) {
text = text[0];
} else {
let message = "";
text.forEach(
(t: string) =>
(message += "<p>" + this.sanitizer.sanitize(SecurityContext.HTML, t) + "</p>"),
);
text = message;
options.enableHtml = true;
}
this.toastrService.show(text, title, options, "toast-" + type);
// noop
}
}

View File

@ -1,27 +1,18 @@
import { import { ChangeDetectorRef, Component, NgZone, OnDestroy, OnInit } from "@angular/core";
ChangeDetectorRef,
Component,
NgZone,
OnDestroy,
OnInit,
SecurityContext,
} from "@angular/core";
import { DomSanitizer } from "@angular/platform-browser";
import { NavigationEnd, Router, RouterOutlet } from "@angular/router"; import { NavigationEnd, Router, RouterOutlet } from "@angular/router";
import { IndividualConfig, ToastrService } from "ngx-toastr"; import { ToastrService } from "ngx-toastr";
import { filter, concatMap, Subject, takeUntil, firstValueFrom } from "rxjs"; import { filter, concatMap, Subject, takeUntil, firstValueFrom } from "rxjs";
import { AuthService } from "@bitwarden/common/auth/abstractions/auth.service"; import { AuthService } from "@bitwarden/common/auth/abstractions/auth.service";
import { BroadcasterService } from "@bitwarden/common/platform/abstractions/broadcaster.service"; import { BroadcasterService } from "@bitwarden/common/platform/abstractions/broadcaster.service";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service";
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service"; import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
import { DialogService, SimpleDialogOptions } from "@bitwarden/components"; import { DialogService, SimpleDialogOptions } from "@bitwarden/components";
import { BrowserApi } from "../platform/browser/browser-api"; import { BrowserApi } from "../platform/browser/browser-api";
import { ZonedMessageListenerService } from "../platform/browser/zoned-message-listener.service"; import { ZonedMessageListenerService } from "../platform/browser/zoned-message-listener.service";
import { BrowserStateService } from "../platform/services/abstractions/browser-state.service"; import { BrowserStateService } from "../platform/services/abstractions/browser-state.service";
import { ForegroundPlatformUtilsService } from "../platform/services/platform-utils/foreground-platform-utils.service";
import { routerTransition } from "./app-routing.animations"; import { routerTransition } from "./app-routing.animations";
import { DesktopSyncVerificationDialogComponent } from "./components/desktop-sync-verification-dialog.component"; import { DesktopSyncVerificationDialogComponent } from "./components/desktop-sync-verification-dialog.component";
@ -48,11 +39,9 @@ export class AppComponent implements OnInit, OnDestroy {
private router: Router, private router: Router,
private stateService: BrowserStateService, private stateService: BrowserStateService,
private cipherService: CipherService, private cipherService: CipherService,
private messagingService: MessagingService,
private changeDetectorRef: ChangeDetectorRef, private changeDetectorRef: ChangeDetectorRef,
private ngZone: NgZone, private ngZone: NgZone,
private sanitizer: DomSanitizer, private platformUtilsService: ForegroundPlatformUtilsService,
private platformUtilsService: PlatformUtilsService,
private dialogService: DialogService, private dialogService: DialogService,
private browserMessagingApi: ZonedMessageListenerService, private browserMessagingApi: ZonedMessageListenerService,
) {} ) {}
@ -219,31 +208,7 @@ export class AppComponent implements OnInit, OnDestroy {
} }
private showToast(msg: any) { private showToast(msg: any) {
let message = ""; this.platformUtilsService.showToast(msg.type, msg.title, msg.text, msg.options);
const options: Partial<IndividualConfig> = {};
if (typeof msg.text === "string") {
message = msg.text;
} else if (msg.text.length === 1) {
message = msg.text[0];
} else {
msg.text.forEach(
(t: string) =>
(message += "<p>" + this.sanitizer.sanitize(SecurityContext.HTML, t) + "</p>"),
);
options.enableHtml = true;
}
if (msg.options != null) {
if (msg.options.trustedHtml === true) {
options.enableHtml = true;
}
if (msg.options.timeout != null && msg.options.timeout > 0) {
options.timeOut = msg.options.timeout;
}
}
this.toastrService.show(message, msg.title, options, "toast-" + msg.type);
} }
private async showDialog(msg: SimpleDialogOptions) { private async showDialog(msg: SimpleDialogOptions) {

View File

@ -1,7 +1,7 @@
import { enableProdMode } from "@angular/core"; import { enableProdMode } from "@angular/core";
import { platformBrowserDynamic } from "@angular/platform-browser-dynamic"; import { platformBrowserDynamic } from "@angular/platform-browser-dynamic";
import BrowserPlatformUtilsService from "../platform/services/browser-platform-utils.service"; import { BrowserPlatformUtilsService } from "../platform/services/platform-utils/browser-platform-utils.service";
require("./scss/popup.scss"); require("./scss/popup.scss");
require("./scss/tailwind.css"); require("./scss/tailwind.css");

View File

@ -1,4 +1,6 @@
import { APP_INITIALIZER, NgModule, NgZone } from "@angular/core"; import { APP_INITIALIZER, NgModule, NgZone } from "@angular/core";
import { DomSanitizer } from "@angular/platform-browser";
import { ToastrService } from "ngx-toastr";
import { UnauthGuard as BaseUnauthGuardService } from "@bitwarden/angular/auth/guards"; import { UnauthGuard as BaseUnauthGuardService } from "@bitwarden/angular/auth/guards";
import { ThemingService } from "@bitwarden/angular/platform/services/theming/theming.service"; import { ThemingService } from "@bitwarden/angular/platform/services/theming/theming.service";
@ -44,7 +46,6 @@ import {
UserNotificationSettingsService, UserNotificationSettingsService,
UserNotificationSettingsServiceAbstraction, UserNotificationSettingsServiceAbstraction,
} from "@bitwarden/common/autofill/services/user-notification-settings.service"; } from "@bitwarden/common/autofill/services/user-notification-settings.service";
import { AppIdService } from "@bitwarden/common/platform/abstractions/app-id.service";
import { ConfigApiServiceAbstraction } from "@bitwarden/common/platform/abstractions/config/config-api.service.abstraction"; import { ConfigApiServiceAbstraction } from "@bitwarden/common/platform/abstractions/config/config-api.service.abstraction";
import { CryptoFunctionService } from "@bitwarden/common/platform/abstractions/crypto-function.service"; import { CryptoFunctionService } from "@bitwarden/common/platform/abstractions/crypto-function.service";
import { CryptoService } from "@bitwarden/common/platform/abstractions/crypto.service"; import { CryptoService } from "@bitwarden/common/platform/abstractions/crypto.service";
@ -118,6 +119,7 @@ import BrowserMessagingPrivateModePopupService from "../../platform/services/bro
import BrowserMessagingService from "../../platform/services/browser-messaging.service"; import BrowserMessagingService from "../../platform/services/browser-messaging.service";
import { BrowserStateService } from "../../platform/services/browser-state.service"; import { BrowserStateService } from "../../platform/services/browser-state.service";
import I18nService from "../../platform/services/i18n.service"; import I18nService from "../../platform/services/i18n.service";
import { ForegroundPlatformUtilsService } from "../../platform/services/platform-utils/foreground-platform-utils.service";
import { ForegroundDerivedStateProvider } from "../../platform/state/foreground-derived-state.provider"; import { ForegroundDerivedStateProvider } from "../../platform/state/foreground-derived-state.provider";
import { ForegroundMemoryStorageService } from "../../platform/storage/foreground-memory-storage.service"; import { ForegroundMemoryStorageService } from "../../platform/storage/foreground-memory-storage.service";
import { BrowserSendService } from "../../services/browser-send.service"; import { BrowserSendService } from "../../services/browser-send.service";
@ -291,8 +293,32 @@ function getBgService<T>(service: keyof MainBackground) {
}, },
{ {
provide: PlatformUtilsService, provide: PlatformUtilsService,
useFactory: getBgService<PlatformUtilsService>("platformUtilsService"), useExisting: ForegroundPlatformUtilsService,
deps: [], },
{
provide: ForegroundPlatformUtilsService,
useClass: ForegroundPlatformUtilsService,
useFactory: (sanitizer: DomSanitizer, toastrService: ToastrService) => {
return new ForegroundPlatformUtilsService(
sanitizer,
toastrService,
(clipboardValue: string, clearMs: number) => {
void BrowserApi.sendMessage("clearClipboard", { clipboardValue, clearMs });
},
async () => {
const response = await BrowserApi.sendMessageWithResponse<{
result: boolean;
error: string;
}>("biometricUnlock");
if (!response.result) {
throw response.error;
}
return response.result;
},
window,
);
},
deps: [DomSanitizer, ToastrService],
}, },
{ {
provide: PasswordStrengthServiceAbstraction, provide: PasswordStrengthServiceAbstraction,
@ -349,7 +375,6 @@ function getBgService<T>(service: keyof MainBackground) {
useClass: BrowserLocalStorageService, useClass: BrowserLocalStorageService,
deps: [], deps: [],
}, },
{ provide: AppIdService, useFactory: getBgService<AppIdService>("appIdService"), deps: [] },
{ {
provide: AutofillService, provide: AutofillService,
useFactory: getBgService<AutofillService>("autofillService"), useFactory: getBgService<AutofillService>("autofillService"),

View File

@ -1,6 +1,8 @@
import { Component, NgZone, OnDestroy, OnInit } from "@angular/core"; import { Component, NgZone, OnDestroy, OnInit } from "@angular/core";
import { Router } from "@angular/router"; import { Router } from "@angular/router";
import { firstValueFrom } from "rxjs";
import { DomainSettingsService } from "@bitwarden/common/autofill/services/domain-settings.service";
import { BroadcasterService } from "@bitwarden/common/platform/abstractions/broadcaster.service"; import { BroadcasterService } from "@bitwarden/common/platform/abstractions/broadcaster.service";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
@ -30,6 +32,7 @@ export class ExcludedDomainsComponent implements OnInit, OnDestroy {
constructor( constructor(
private stateService: StateService, private stateService: StateService,
private domainSettingsService: DomainSettingsService,
private i18nService: I18nService, private i18nService: I18nService,
private router: Router, private router: Router,
private broadcasterService: BroadcasterService, private broadcasterService: BroadcasterService,
@ -40,7 +43,7 @@ export class ExcludedDomainsComponent implements OnInit, OnDestroy {
} }
async ngOnInit() { async ngOnInit() {
const savedDomains = await this.stateService.getNeverDomains(); const savedDomains = await firstValueFrom(this.domainSettingsService.neverDomains$);
if (savedDomains) { if (savedDomains) {
for (const uri of Object.keys(savedDomains)) { for (const uri of Object.keys(savedDomains)) {
this.excludedDomains.push({ uri: uri, showCurrentUris: false }); this.excludedDomains.push({ uri: uri, showCurrentUris: false });
@ -107,7 +110,7 @@ export class ExcludedDomainsComponent implements OnInit, OnDestroy {
} }
} }
await this.stateService.setNeverDomains(savedDomains); await this.domainSettingsService.setNeverDomains(savedDomains);
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling. // 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 // eslint-disable-next-line @typescript-eslint/no-floating-promises
this.router.navigate(["/tabs/settings"]); this.router.navigate(["/tabs/settings"]);

View File

@ -5,14 +5,18 @@ import { AbstractThemingService } from "@bitwarden/angular/platform/services/the
import { SettingsService } from "@bitwarden/common/abstractions/settings.service"; import { SettingsService } from "@bitwarden/common/abstractions/settings.service";
import { AutofillSettingsServiceAbstraction } from "@bitwarden/common/autofill/services/autofill-settings.service"; import { AutofillSettingsServiceAbstraction } from "@bitwarden/common/autofill/services/autofill-settings.service";
import { BadgeSettingsServiceAbstraction } from "@bitwarden/common/autofill/services/badge-settings.service"; import { BadgeSettingsServiceAbstraction } from "@bitwarden/common/autofill/services/badge-settings.service";
import { DomainSettingsService } from "@bitwarden/common/autofill/services/domain-settings.service";
import { UserNotificationSettingsServiceAbstraction } from "@bitwarden/common/autofill/services/user-notification-settings.service"; import { UserNotificationSettingsServiceAbstraction } from "@bitwarden/common/autofill/services/user-notification-settings.service";
import { ClearClipboardDelaySetting } from "@bitwarden/common/autofill/types"; import { ClearClipboardDelaySetting } from "@bitwarden/common/autofill/types";
import {
UriMatchStrategy,
UriMatchStrategySetting,
} from "@bitwarden/common/models/domain/domain-service";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service"; import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service";
import { StateService } from "@bitwarden/common/platform/abstractions/state.service"; import { StateService } from "@bitwarden/common/platform/abstractions/state.service";
import { ThemeType } from "@bitwarden/common/platform/enums"; import { ThemeType } from "@bitwarden/common/platform/enums";
import { VaultSettingsService } from "@bitwarden/common/vault/abstractions/vault-settings/vault-settings.service"; import { VaultSettingsService } from "@bitwarden/common/vault/abstractions/vault-settings/vault-settings.service";
import { UriMatchType } from "@bitwarden/common/vault/enums";
import { enableAccountSwitching } from "../../platform/flags"; import { enableAccountSwitching } from "../../platform/flags";
@ -36,7 +40,7 @@ export class OptionsComponent implements OnInit {
showClearClipboard = true; showClearClipboard = true;
theme: ThemeType; theme: ThemeType;
themeOptions: any[]; themeOptions: any[];
defaultUriMatch = UriMatchType.Domain; defaultUriMatch: UriMatchStrategySetting = UriMatchStrategy.Domain;
uriMatchOptions: any[]; uriMatchOptions: any[];
clearClipboard: ClearClipboardDelaySetting; clearClipboard: ClearClipboardDelaySetting;
clearClipboardOptions: any[]; clearClipboardOptions: any[];
@ -50,6 +54,7 @@ export class OptionsComponent implements OnInit {
private stateService: StateService, private stateService: StateService,
private userNotificationSettingsService: UserNotificationSettingsServiceAbstraction, private userNotificationSettingsService: UserNotificationSettingsServiceAbstraction,
private autofillSettingsService: AutofillSettingsServiceAbstraction, private autofillSettingsService: AutofillSettingsServiceAbstraction,
private domainSettingsService: DomainSettingsService,
private badgeSettingsService: BadgeSettingsServiceAbstraction, private badgeSettingsService: BadgeSettingsServiceAbstraction,
i18nService: I18nService, i18nService: I18nService,
private themingService: AbstractThemingService, private themingService: AbstractThemingService,
@ -64,12 +69,12 @@ export class OptionsComponent implements OnInit {
{ name: i18nService.t("solarizedDark"), value: ThemeType.SolarizedDark }, { name: i18nService.t("solarizedDark"), value: ThemeType.SolarizedDark },
]; ];
this.uriMatchOptions = [ this.uriMatchOptions = [
{ name: i18nService.t("baseDomain"), value: UriMatchType.Domain }, { name: i18nService.t("baseDomain"), value: UriMatchStrategy.Domain },
{ name: i18nService.t("host"), value: UriMatchType.Host }, { name: i18nService.t("host"), value: UriMatchStrategy.Host },
{ name: i18nService.t("startsWith"), value: UriMatchType.StartsWith }, { name: i18nService.t("startsWith"), value: UriMatchStrategy.StartsWith },
{ name: i18nService.t("regEx"), value: UriMatchType.RegularExpression }, { name: i18nService.t("regEx"), value: UriMatchStrategy.RegularExpression },
{ name: i18nService.t("exact"), value: UriMatchType.Exact }, { name: i18nService.t("exact"), value: UriMatchStrategy.Exact },
{ name: i18nService.t("never"), value: UriMatchType.Never }, { name: i18nService.t("never"), value: UriMatchStrategy.Never },
]; ];
this.clearClipboardOptions = [ this.clearClipboardOptions = [
{ name: i18nService.t("never"), value: null }, { name: i18nService.t("never"), value: null },
@ -122,8 +127,10 @@ export class OptionsComponent implements OnInit {
this.theme = await this.stateService.getTheme(); this.theme = await this.stateService.getTheme();
const defaultUriMatch = await this.stateService.getDefaultUriMatch(); const defaultUriMatch = await firstValueFrom(
this.defaultUriMatch = defaultUriMatch == null ? UriMatchType.Domain : defaultUriMatch; this.domainSettingsService.defaultUriMatchStrategy$,
);
this.defaultUriMatch = defaultUriMatch == null ? UriMatchStrategy.Domain : defaultUriMatch;
this.clearClipboard = await firstValueFrom(this.autofillSettingsService.clearClipboardDelay$); this.clearClipboard = await firstValueFrom(this.autofillSettingsService.clearClipboardDelay$);
} }
@ -182,10 +189,6 @@ export class OptionsComponent implements OnInit {
await this.themingService.updateConfiguredTheme(this.theme); await this.themingService.updateConfiguredTheme(this.theme);
} }
async saveDefaultUriMatch() {
await this.stateService.setDefaultUriMatch(this.defaultUriMatch);
}
async saveClearClipboard() { async saveClearClipboard() {
await this.autofillSettingsService.setClearClipboardDelay(this.clearClipboard); await this.autofillSettingsService.setClearClipboardDelay(this.clearClipboard);
} }

View File

@ -397,7 +397,7 @@ export class SettingsComponent implements OnInit {
// Handle connection errors // Handle connection errors
this.form.controls.biometric.setValue(false); this.form.controls.biometric.setValue(false);
const error = BiometricErrors[e as BiometricErrorTypes]; const error = BiometricErrors[e.message as BiometricErrorTypes];
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling. // 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 // eslint-disable-next-line @typescript-eslint/no-floating-promises

View File

@ -1,15 +1,11 @@
import { BehaviorSubject } from "rxjs"; import { BehaviorSubject } from "rxjs";
import { AccountSettingsSettings } from "@bitwarden/common/platform/models/domain/account";
import { SettingsService } from "@bitwarden/common/services/settings.service"; import { SettingsService } from "@bitwarden/common/services/settings.service";
import { browserSession, sessionSync } from "../platform/decorators/session-sync-observable"; import { browserSession, sessionSync } from "../platform/decorators/session-sync-observable";
@browserSession @browserSession
export class BrowserSettingsService extends SettingsService { export class BrowserSettingsService extends SettingsService {
@sessionSync({ initializer: (obj: string[][]) => obj })
protected _settings: BehaviorSubject<AccountSettingsSettings>;
@sessionSync({ initializer: (b: boolean) => b }) @sessionSync({ initializer: (b: boolean) => b })
protected _disableFavicon: BehaviorSubject<boolean>; protected _disableFavicon: BehaviorSubject<boolean>;
} }

View File

@ -5,6 +5,10 @@ import {
AutofillSettingsServiceInitOptions, AutofillSettingsServiceInitOptions,
autofillSettingsServiceFactory, autofillSettingsServiceFactory,
} from "../../../autofill/background/service_factories/autofill-settings-service.factory"; } from "../../../autofill/background/service_factories/autofill-settings-service.factory";
import {
DomainSettingsServiceInitOptions,
domainSettingsServiceFactory,
} from "../../../autofill/background/service_factories/domain-settings-service.factory";
import { import {
CipherFileUploadServiceInitOptions, CipherFileUploadServiceInitOptions,
cipherFileUploadServiceFactory, cipherFileUploadServiceFactory,
@ -13,10 +17,6 @@ import {
searchServiceFactory, searchServiceFactory,
SearchServiceInitOptions, SearchServiceInitOptions,
} from "../../../background/service-factories/search-service.factory"; } from "../../../background/service-factories/search-service.factory";
import {
SettingsServiceInitOptions,
settingsServiceFactory,
} from "../../../background/service-factories/settings-service.factory";
import { import {
apiServiceFactory, apiServiceFactory,
ApiServiceInitOptions, ApiServiceInitOptions,
@ -52,13 +52,13 @@ type CipherServiceFactoryOptions = FactoryOptions;
export type CipherServiceInitOptions = CipherServiceFactoryOptions & export type CipherServiceInitOptions = CipherServiceFactoryOptions &
CryptoServiceInitOptions & CryptoServiceInitOptions &
SettingsServiceInitOptions &
ApiServiceInitOptions & ApiServiceInitOptions &
CipherFileUploadServiceInitOptions & CipherFileUploadServiceInitOptions &
I18nServiceInitOptions & I18nServiceInitOptions &
SearchServiceInitOptions & SearchServiceInitOptions &
StateServiceInitOptions & StateServiceInitOptions &
AutofillSettingsServiceInitOptions & AutofillSettingsServiceInitOptions &
DomainSettingsServiceInitOptions &
EncryptServiceInitOptions & EncryptServiceInitOptions &
ConfigServiceInitOptions; ConfigServiceInitOptions;
@ -73,7 +73,7 @@ export function cipherServiceFactory(
async () => async () =>
new CipherService( new CipherService(
await cryptoServiceFactory(cache, opts), await cryptoServiceFactory(cache, opts),
await settingsServiceFactory(cache, opts), await domainSettingsServiceFactory(cache, opts),
await apiServiceFactory(cache, opts), await apiServiceFactory(cache, opts),
await i18nServiceFactory(cache, opts), await i18nServiceFactory(cache, opts),
await searchServiceFactory(cache, opts), await searchServiceFactory(cache, opts),

View File

@ -3,6 +3,7 @@ import { ConnectedPosition } from "@angular/cdk/overlay";
import { Component } from "@angular/core"; import { Component } from "@angular/core";
import { firstValueFrom } from "rxjs"; import { firstValueFrom } from "rxjs";
import { DomainSettingsService } from "@bitwarden/common/autofill/services/domain-settings.service";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
import { StateService } from "@bitwarden/common/platform/abstractions/state.service"; import { StateService } from "@bitwarden/common/platform/abstractions/state.service";
@ -52,6 +53,7 @@ export class Fido2UseBrowserLinkComponent {
constructor( constructor(
private stateService: StateService, private stateService: StateService,
private domainSettingsService: DomainSettingsService,
private platformUtilsService: PlatformUtilsService, private platformUtilsService: PlatformUtilsService,
private i18nService: I18nService, private i18nService: I18nService,
) {} ) {}
@ -89,7 +91,7 @@ export class Fido2UseBrowserLinkComponent {
* @param uri - The domain uri to exclude from future FIDO2 prompts. * @param uri - The domain uri to exclude from future FIDO2 prompts.
*/ */
private async handleDomainExclusion(uri: string) { private async handleDomainExclusion(uri: string) {
const exisitingDomains = await this.stateService.getNeverDomains(); const exisitingDomains = await firstValueFrom(this.domainSettingsService.neverDomains$);
const validDomain = Utils.getHostname(uri); const validDomain = Utils.getHostname(uri);
const savedDomains: { [name: string]: unknown } = { const savedDomains: { [name: string]: unknown } = {
@ -97,9 +99,7 @@ export class Fido2UseBrowserLinkComponent {
}; };
savedDomains[validDomain] = null; savedDomains[validDomain] = null;
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling. await this.domainSettingsService.setNeverDomains(savedDomains);
// eslint-disable-next-line @typescript-eslint/no-floating-promises
this.stateService.setNeverDomains(savedDomains);
this.platformUtilsService.showToast( this.platformUtilsService.showToast(
"success", "success",

View File

@ -5,6 +5,7 @@ import {
combineLatest, combineLatest,
concatMap, concatMap,
filter, filter,
firstValueFrom,
map, map,
Observable, Observable,
Subject, Subject,
@ -13,7 +14,7 @@ import {
} from "rxjs"; } from "rxjs";
import { SearchService } from "@bitwarden/common/abstractions/search.service"; import { SearchService } from "@bitwarden/common/abstractions/search.service";
import { SettingsService } from "@bitwarden/common/abstractions/settings.service"; import { DomainSettingsService } from "@bitwarden/common/autofill/services/domain-settings.service";
import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service"; import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
@ -72,7 +73,7 @@ export class Fido2Component implements OnInit, OnDestroy {
private cipherService: CipherService, private cipherService: CipherService,
private passwordRepromptService: PasswordRepromptService, private passwordRepromptService: PasswordRepromptService,
private platformUtilsService: PlatformUtilsService, private platformUtilsService: PlatformUtilsService,
private settingsService: SettingsService, private domainSettingsService: DomainSettingsService,
private searchService: SearchService, private searchService: SearchService,
private logService: LogService, private logService: LogService,
private dialogService: DialogService, private dialogService: DialogService,
@ -133,7 +134,9 @@ export class Fido2Component implements OnInit, OnDestroy {
concatMap(async (message) => { concatMap(async (message) => {
switch (message.type) { switch (message.type) {
case "ConfirmNewCredentialRequest": { case "ConfirmNewCredentialRequest": {
const equivalentDomains = this.settingsService.getEquivalentDomains(this.url); const equivalentDomains = await firstValueFrom(
this.domainSettingsService.getUrlEquivalentDomains(this.url),
);
this.ciphers = (await this.cipherService.getAllDecrypted()).filter( this.ciphers = (await this.cipherService.getAllDecrypted()).filter(
(cipher) => cipher.type === CipherType.Login && !cipher.isDeleted, (cipher) => cipher.type === CipherType.Login && !cipher.isDeleted,
@ -317,7 +320,9 @@ export class Fido2Component implements OnInit, OnDestroy {
this.ciphers, this.ciphers,
); );
} else { } else {
const equivalentDomains = this.settingsService.getEquivalentDomains(this.url); const equivalentDomains = await firstValueFrom(
this.domainSettingsService.getUrlEquivalentDomains(this.url),
);
this.displayedCiphers = this.ciphers.filter((cipher) => this.displayedCiphers = this.ciphers.filter((cipher) =>
cipher.login.matchesUri(this.url, equivalentDomains), cipher.login.matchesUri(this.url, equivalentDomains),
); );

View File

@ -1,6 +1,5 @@
const path = require("path"); const path = require("path");
const webpack = require("webpack"); const webpack = require("webpack");
const { CleanWebpackPlugin } = require("clean-webpack-plugin");
const HtmlWebpackPlugin = require("html-webpack-plugin"); const HtmlWebpackPlugin = require("html-webpack-plugin");
const CopyWebpackPlugin = require("copy-webpack-plugin"); const CopyWebpackPlugin = require("copy-webpack-plugin");
const MiniCssExtractPlugin = require("mini-css-extract-plugin"); const MiniCssExtractPlugin = require("mini-css-extract-plugin");
@ -138,9 +137,6 @@ const plugins = [
entryModule: "src/popup/app.module#AppModule", entryModule: "src/popup/app.module#AppModule",
sourceMap: true, sourceMap: true,
}), }),
new CleanWebpackPlugin({
cleanAfterEveryBuildPatterns: ["!popup/fonts/**/*"],
}),
new webpack.ProvidePlugin({ new webpack.ProvidePlugin({
process: "process/browser.js", process: "process/browser.js",
}), }),
@ -244,6 +240,7 @@ const mainConfig = {
output: { output: {
filename: "[name].js", filename: "[name].js",
path: path.resolve(__dirname, "build"), path: path.resolve(__dirname, "build"),
clean: true,
}, },
module: { module: {
noParse: /\.wasm$/, noParse: /\.wasm$/,

View File

@ -273,8 +273,8 @@ export class LoginCommand {
selectedProvider.type === TwoFactorProviderType.Email selectedProvider.type === TwoFactorProviderType.Email
) { ) {
const emailReq = new TwoFactorEmailRequest(); const emailReq = new TwoFactorEmailRequest();
emailReq.email = this.loginStrategyService.email; emailReq.email = await this.loginStrategyService.getEmail();
emailReq.masterPasswordHash = this.loginStrategyService.masterPasswordHash; emailReq.masterPasswordHash = await this.loginStrategyService.getMasterPasswordHash();
await this.apiService.postTwoFactorEmail(emailReq); await this.apiService.postTwoFactorEmail(emailReq);
} }

View File

@ -35,6 +35,10 @@ import { TwoFactorService } from "@bitwarden/common/auth/services/two-factor.ser
import { UserVerificationApiService } from "@bitwarden/common/auth/services/user-verification/user-verification-api.service"; import { UserVerificationApiService } from "@bitwarden/common/auth/services/user-verification/user-verification-api.service";
import { UserVerificationService } from "@bitwarden/common/auth/services/user-verification/user-verification.service"; import { UserVerificationService } from "@bitwarden/common/auth/services/user-verification/user-verification.service";
import { AutofillSettingsServiceAbstraction } from "@bitwarden/common/autofill/services/autofill-settings.service"; import { AutofillSettingsServiceAbstraction } from "@bitwarden/common/autofill/services/autofill-settings.service";
import {
DefaultDomainSettingsService,
DomainSettingsService,
} from "@bitwarden/common/autofill/services/domain-settings.service";
import { ClientType } from "@bitwarden/common/enums"; import { ClientType } from "@bitwarden/common/enums";
import { ConfigApiServiceAbstraction } from "@bitwarden/common/platform/abstractions/config/config-api.service.abstraction"; import { ConfigApiServiceAbstraction } from "@bitwarden/common/platform/abstractions/config/config-api.service.abstraction";
import { KeyGenerationService as KeyGenerationServiceAbstraction } from "@bitwarden/common/platform/abstractions/key-generation.service"; import { KeyGenerationService as KeyGenerationServiceAbstraction } from "@bitwarden/common/platform/abstractions/key-generation.service";
@ -190,6 +194,7 @@ export class Main {
pinCryptoService: PinCryptoServiceAbstraction; pinCryptoService: PinCryptoServiceAbstraction;
stateService: StateService; stateService: StateService;
autofillSettingsService: AutofillSettingsServiceAbstraction; autofillSettingsService: AutofillSettingsServiceAbstraction;
domainSettingsService: DomainSettingsService;
organizationService: OrganizationService; organizationService: OrganizationService;
providerService: ProviderService; providerService: ProviderService;
twoFactorService: TwoFactorService; twoFactorService: TwoFactorService;
@ -333,7 +338,7 @@ export class Main {
this.stateProvider, this.stateProvider,
); );
this.appIdService = new AppIdService(this.storageService); this.appIdService = new AppIdService(this.globalStateProvider);
this.tokenService = new TokenService(this.stateService); this.tokenService = new TokenService(this.stateService);
const customUserAgent = const customUserAgent =
@ -358,6 +363,7 @@ export class Main {
this.containerService = new ContainerService(this.cryptoService, this.encryptService); this.containerService = new ContainerService(this.cryptoService, this.encryptService);
this.settingsService = new SettingsService(this.stateService); this.settingsService = new SettingsService(this.stateService);
this.domainSettingsService = new DefaultDomainSettingsService(this.stateProvider);
this.fileUploadService = new FileUploadService(this.logService); this.fileUploadService = new FileUploadService(this.logService);
@ -458,6 +464,7 @@ export class Main {
this.policyService, this.policyService,
this.deviceTrustCryptoService, this.deviceTrustCryptoService,
this.authRequestService, this.authRequestService,
this.globalStateProvider,
); );
this.authService = new AuthService( this.authService = new AuthService(
@ -480,7 +487,7 @@ export class Main {
this.cipherService = new CipherService( this.cipherService = new CipherService(
this.cryptoService, this.cryptoService,
this.settingsService, this.domainSettingsService,
this.apiService, this.apiService,
this.i18nService, this.i18nService,
this.searchService, this.searchService,
@ -551,7 +558,7 @@ export class Main {
this.syncService = new SyncService( this.syncService = new SyncService(
this.apiService, this.apiService,
this.settingsService, this.domainSettingsService,
this.folderService, this.folderService,
this.cipherService, this.cipherService,
this.cryptoService, this.cryptoService,
@ -647,7 +654,6 @@ export class Main {
await Promise.all([ await Promise.all([
this.syncService.setLastSync(new Date(0)), this.syncService.setLastSync(new Date(0)),
this.cryptoService.clearKeys(), this.cryptoService.clearKeys(),
this.settingsService.clear(userId),
this.cipherService.clear(userId), this.cipherService.clear(userId),
this.folderService.clear(userId), this.folderService.clear(userId),
this.collectionService.clear(userId as UserId), this.collectionService.clear(userId as UserId),

View File

@ -1,6 +1,5 @@
const path = require("path"); const path = require("path");
const webpack = require("webpack"); const webpack = require("webpack");
const { CleanWebpackPlugin } = require("clean-webpack-plugin");
const CopyWebpackPlugin = require("copy-webpack-plugin"); const CopyWebpackPlugin = require("copy-webpack-plugin");
const nodeExternals = require("webpack-node-externals"); const nodeExternals = require("webpack-node-externals");
const TsconfigPathsPlugin = require("tsconfig-paths-webpack-plugin"); const TsconfigPathsPlugin = require("tsconfig-paths-webpack-plugin");
@ -23,7 +22,6 @@ const moduleRules = [
]; ];
const plugins = [ const plugins = [
new CleanWebpackPlugin(),
new CopyWebpackPlugin({ new CopyWebpackPlugin({
patterns: [{ from: "./src/locales", to: "locales" }], patterns: [{ from: "./src/locales", to: "locales" }],
}), }),
@ -71,6 +69,7 @@ const webpackConfig = {
output: { output: {
filename: "[name].js", filename: "[name].js",
path: path.resolve(__dirname, "build"), path: path.resolve(__dirname, "build"),
clean: true,
}, },
module: { rules: moduleRules }, module: { rules: moduleRules },
plugins: plugins, plugins: plugins,

View File

@ -1,7 +1,7 @@
{ {
"name": "@bitwarden/desktop", "name": "@bitwarden/desktop",
"description": "A secure and free password manager for all of your devices.", "description": "A secure and free password manager for all of your devices.",
"version": "2024.3.0", "version": "2024.3.1",
"keywords": [ "keywords": [
"bitwarden", "bitwarden",
"password", "password",

View File

@ -577,7 +577,6 @@ export class AppComponent implements OnInit, OnDestroy {
await this.eventUploadService.uploadEvents(userBeingLoggedOut); await this.eventUploadService.uploadEvents(userBeingLoggedOut);
await this.syncService.setLastSync(new Date(0), userBeingLoggedOut); await this.syncService.setLastSync(new Date(0), userBeingLoggedOut);
await this.cryptoService.clearKeys(userBeingLoggedOut); await this.cryptoService.clearKeys(userBeingLoggedOut);
await this.settingsService.clear(userBeingLoggedOut);
await this.cipherService.clear(userBeingLoggedOut); await this.cipherService.clear(userBeingLoggedOut);
await this.folderService.clear(userBeingLoggedOut); await this.folderService.clear(userBeingLoggedOut);
await this.collectionService.clear(userBeingLoggedOut); await this.collectionService.clear(userBeingLoggedOut);

View File

@ -1,12 +1,12 @@
{ {
"name": "@bitwarden/desktop", "name": "@bitwarden/desktop",
"version": "2024.3.0", "version": "2024.3.1",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "@bitwarden/desktop", "name": "@bitwarden/desktop",
"version": "2024.3.0", "version": "2024.3.1",
"license": "GPL-3.0", "license": "GPL-3.0",
"dependencies": { "dependencies": {
"@bitwarden/desktop-native": "file:../desktop_native" "@bitwarden/desktop-native": "file:../desktop_native"

View File

@ -2,7 +2,7 @@
"name": "@bitwarden/desktop", "name": "@bitwarden/desktop",
"productName": "Bitwarden", "productName": "Bitwarden",
"description": "A secure and free password manager for all of your devices.", "description": "A secure and free password manager for all of your devices.",
"version": "2024.3.0", "version": "2024.3.1",
"author": "Bitwarden Inc. <hello@bitwarden.com> (https://bitwarden.com)", "author": "Bitwarden Inc. <hello@bitwarden.com> (https://bitwarden.com)",
"homepage": "https://bitwarden.com", "homepage": "https://bitwarden.com",
"license": "GPL-3.0", "license": "GPL-3.0",

View File

@ -1,7 +1,6 @@
const path = require("path"); const path = require("path");
const { merge } = require("webpack-merge"); const { merge } = require("webpack-merge");
const CopyWebpackPlugin = require("copy-webpack-plugin"); const CopyWebpackPlugin = require("copy-webpack-plugin");
const { CleanWebpackPlugin } = require("clean-webpack-plugin");
const TsconfigPathsPlugin = require("tsconfig-paths-webpack-plugin"); const TsconfigPathsPlugin = require("tsconfig-paths-webpack-plugin");
const configurator = require("./config/config"); const configurator = require("./config/config");
const { EnvironmentPlugin } = require("webpack"); const { EnvironmentPlugin } = require("webpack");

View File

@ -1,7 +1,6 @@
const path = require("path"); const path = require("path");
const { merge } = require("webpack-merge"); const { merge } = require("webpack-merge");
const CopyWebpackPlugin = require("copy-webpack-plugin"); const CopyWebpackPlugin = require("copy-webpack-plugin");
const { CleanWebpackPlugin } = require("clean-webpack-plugin");
const TsconfigPathsPlugin = require("tsconfig-paths-webpack-plugin"); const TsconfigPathsPlugin = require("tsconfig-paths-webpack-plugin");
const configurator = require("./config/config"); const configurator = require("./config/config");
const { EnvironmentPlugin } = require("webpack"); const { EnvironmentPlugin } = require("webpack");

View File

@ -275,7 +275,6 @@ export class AppComponent implements OnDestroy, OnInit {
await Promise.all([ await Promise.all([
this.syncService.setLastSync(new Date(0)), this.syncService.setLastSync(new Date(0)),
this.cryptoService.clearKeys(), this.cryptoService.clearKeys(),
this.settingsService.clear(userId),
this.cipherService.clear(userId), this.cipherService.clear(userId),
this.folderService.clear(userId), this.folderService.clear(userId),
this.collectionService.clear(userId), this.collectionService.clear(userId),

View File

@ -2,7 +2,6 @@ const fs = require("fs");
const path = require("path"); const path = require("path");
const { AngularWebpackPlugin } = require("@ngtools/webpack"); const { AngularWebpackPlugin } = require("@ngtools/webpack");
const { CleanWebpackPlugin } = require("clean-webpack-plugin");
const CopyWebpackPlugin = require("copy-webpack-plugin"); const CopyWebpackPlugin = require("copy-webpack-plugin");
const HtmlWebpackInjector = require("html-webpack-injector"); const HtmlWebpackInjector = require("html-webpack-injector");
const HtmlWebpackPlugin = require("html-webpack-plugin"); const HtmlWebpackPlugin = require("html-webpack-plugin");
@ -87,7 +86,6 @@ const moduleRules = [
]; ];
const plugins = [ const plugins = [
new CleanWebpackPlugin(),
new HtmlWebpackPlugin({ new HtmlWebpackPlugin({
template: "./src/index.html", template: "./src/index.html",
filename: "index.html", filename: "index.html",
@ -371,6 +369,7 @@ const webpackConfig = {
output: { output: {
filename: "[name].[contenthash].js", filename: "[name].[contenthash].js",
path: path.resolve(__dirname, "build"), path: path.resolve(__dirname, "build"),
clean: true,
}, },
module: { module: {
noParse: /\.wasm$/, noParse: /\.wasm$/,

View File

@ -99,8 +99,7 @@ export class LoginViaAuthRequestComponent
} }
//gets signalR push notification //gets signalR push notification
this.loginStrategyService this.loginStrategyService.authRequestPushNotification$
.getPushNotificationObs$()
.pipe(takeUntil(this.destroy$)) .pipe(takeUntil(this.destroy$))
.subscribe((id) => { .subscribe((id) => {
// Only fires on approval currently // Only fires on approval currently

View File

@ -1,6 +1,7 @@
import { Directive, Inject, OnDestroy, OnInit } from "@angular/core"; import { Directive, Inject, OnDestroy, OnInit } from "@angular/core";
import { ActivatedRoute, NavigationExtras, Router } from "@angular/router"; import { ActivatedRoute, NavigationExtras, Router } from "@angular/router";
import * as DuoWebSDK from "duo_web_sdk"; import * as DuoWebSDK from "duo_web_sdk";
import { firstValueFrom } from "rxjs";
import { first } from "rxjs/operators"; import { first } from "rxjs/operators";
// eslint-disable-next-line no-restricted-imports // eslint-disable-next-line no-restricted-imports
@ -10,6 +11,7 @@ import { ApiService } from "@bitwarden/common/abstractions/api.service";
import { LoginService } from "@bitwarden/common/auth/abstractions/login.service"; import { LoginService } from "@bitwarden/common/auth/abstractions/login.service";
import { SsoLoginServiceAbstraction } from "@bitwarden/common/auth/abstractions/sso-login.service.abstraction"; import { SsoLoginServiceAbstraction } from "@bitwarden/common/auth/abstractions/sso-login.service.abstraction";
import { TwoFactorService } from "@bitwarden/common/auth/abstractions/two-factor.service"; import { TwoFactorService } from "@bitwarden/common/auth/abstractions/two-factor.service";
import { AuthenticationType } from "@bitwarden/common/auth/enums/authentication-type";
import { TwoFactorProviderType } from "@bitwarden/common/auth/enums/two-factor-provider-type"; import { TwoFactorProviderType } from "@bitwarden/common/auth/enums/two-factor-provider-type";
import { AuthResult } from "@bitwarden/common/auth/models/domain/auth-result"; import { AuthResult } from "@bitwarden/common/auth/models/domain/auth-result";
import { ForceSetPasswordReason } from "@bitwarden/common/auth/models/domain/force-set-password-reason"; import { ForceSetPasswordReason } from "@bitwarden/common/auth/models/domain/force-set-password-reason";
@ -92,7 +94,7 @@ export class TwoFactorComponent extends CaptchaProtectedComponent implements OnI
} }
async ngOnInit() { async ngOnInit() {
if (!this.authing || this.twoFactorService.getProviders() == null) { if (!(await this.authing()) || this.twoFactorService.getProviders() == null) {
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling. // 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 // eslint-disable-next-line @typescript-eslint/no-floating-promises
this.router.navigate([this.loginRoute]); this.router.navigate([this.loginRoute]);
@ -105,7 +107,7 @@ export class TwoFactorComponent extends CaptchaProtectedComponent implements OnI
} }
}); });
if (this.needsLock) { if (await this.needsLock()) {
this.successRoute = "lock"; this.successRoute = "lock";
} }
@ -426,7 +428,7 @@ export class TwoFactorComponent extends CaptchaProtectedComponent implements OnI
return; return;
} }
if (this.loginStrategyService.email == null) { if ((await this.loginStrategyService.getEmail()) == null) {
this.platformUtilsService.showToast( this.platformUtilsService.showToast(
"error", "error",
this.i18nService.t("errorOccurred"), this.i18nService.t("errorOccurred"),
@ -437,12 +439,13 @@ export class TwoFactorComponent extends CaptchaProtectedComponent implements OnI
try { try {
const request = new TwoFactorEmailRequest(); const request = new TwoFactorEmailRequest();
request.email = this.loginStrategyService.email; request.email = await this.loginStrategyService.getEmail();
request.masterPasswordHash = this.loginStrategyService.masterPasswordHash; request.masterPasswordHash = await this.loginStrategyService.getMasterPasswordHash();
request.ssoEmail2FaSessionToken = this.loginStrategyService.ssoEmail2FaSessionToken; request.ssoEmail2FaSessionToken =
await this.loginStrategyService.getSsoEmail2FaSessionToken();
request.deviceIdentifier = await this.appIdService.getAppId(); request.deviceIdentifier = await this.appIdService.getAppId();
request.authRequestAccessCode = this.loginStrategyService.accessCode; request.authRequestAccessCode = await this.loginStrategyService.getAccessCode();
request.authRequestId = this.loginStrategyService.authRequestId; request.authRequestId = await this.loginStrategyService.getAuthRequestId();
this.emailPromise = this.apiService.postTwoFactorEmail(request); this.emailPromise = this.apiService.postTwoFactorEmail(request);
await this.emailPromise; await this.emailPromise;
if (doToast) { if (doToast) {
@ -476,20 +479,13 @@ export class TwoFactorComponent extends CaptchaProtectedComponent implements OnI
} }
} }
get authing(): boolean { private async authing(): Promise<boolean> {
return ( return (await firstValueFrom(this.loginStrategyService.currentAuthType$)) !== null;
this.loginStrategyService.authingWithPassword() ||
this.loginStrategyService.authingWithSso() ||
this.loginStrategyService.authingWithUserApiKey() ||
this.loginStrategyService.authingWithPasswordless()
);
} }
get needsLock(): boolean { private async needsLock(): Promise<boolean> {
return ( const authType = await firstValueFrom(this.loginStrategyService.currentAuthType$);
this.loginStrategyService.authingWithSso() || return authType == AuthenticationType.Sso || authType == AuthenticationType.UserApiKey;
this.loginStrategyService.authingWithUserApiKey()
);
} }
// implemented in clients // implemented in clients

View File

@ -90,6 +90,10 @@ import {
BadgeSettingsServiceAbstraction, BadgeSettingsServiceAbstraction,
BadgeSettingsService, BadgeSettingsService,
} from "@bitwarden/common/autofill/services/badge-settings.service"; } from "@bitwarden/common/autofill/services/badge-settings.service";
import {
DomainSettingsService,
DefaultDomainSettingsService,
} from "@bitwarden/common/autofill/services/domain-settings.service";
import { BillingApiServiceAbstraction } from "@bitwarden/common/billing/abstractions/billilng-api.service.abstraction"; import { BillingApiServiceAbstraction } from "@bitwarden/common/billing/abstractions/billilng-api.service.abstraction";
import { OrganizationBillingServiceAbstraction } from "@bitwarden/common/billing/abstractions/organization-billing.service"; import { OrganizationBillingServiceAbstraction } from "@bitwarden/common/billing/abstractions/organization-billing.service";
import { PaymentMethodWarningsServiceAbstraction } from "@bitwarden/common/billing/abstractions/payment-method-warnings-service.abstraction"; import { PaymentMethodWarningsServiceAbstraction } from "@bitwarden/common/billing/abstractions/payment-method-warnings-service.abstraction";
@ -290,7 +294,7 @@ import { ModalService } from "./modal.service";
{ {
provide: AppIdServiceAbstraction, provide: AppIdServiceAbstraction,
useClass: AppIdService, useClass: AppIdService,
deps: [AbstractStorageService], deps: [GlobalStateProvider],
}, },
{ {
provide: AuditServiceAbstraction, provide: AuditServiceAbstraction,
@ -328,6 +332,7 @@ import { ModalService } from "./modal.service";
PolicyServiceAbstraction, PolicyServiceAbstraction,
DeviceTrustCryptoServiceAbstraction, DeviceTrustCryptoServiceAbstraction,
AuthRequestServiceAbstraction, AuthRequestServiceAbstraction,
GlobalStateProvider,
], ],
}, },
{ {
@ -350,6 +355,7 @@ import { ModalService } from "./modal.service";
searchService: SearchServiceAbstraction, searchService: SearchServiceAbstraction,
stateService: StateServiceAbstraction, stateService: StateServiceAbstraction,
autofillSettingsService: AutofillSettingsServiceAbstraction, autofillSettingsService: AutofillSettingsServiceAbstraction,
domainSettingsService: DomainSettingsService,
encryptService: EncryptService, encryptService: EncryptService,
fileUploadService: CipherFileUploadServiceAbstraction, fileUploadService: CipherFileUploadServiceAbstraction,
configService: ConfigServiceAbstraction, configService: ConfigServiceAbstraction,
@ -357,7 +363,7 @@ import { ModalService } from "./modal.service";
) => ) =>
new CipherService( new CipherService(
cryptoService, cryptoService,
settingsService, domainSettingsService,
apiService, apiService,
i18nService, i18nService,
searchService, searchService,
@ -739,7 +745,7 @@ import { ModalService } from "./modal.service";
useClass: PasswordResetEnrollmentServiceImplementation, useClass: PasswordResetEnrollmentServiceImplementation,
deps: [ deps: [
OrganizationApiServiceAbstraction, OrganizationApiServiceAbstraction,
StateServiceAbstraction, AccountServiceAbstraction,
CryptoServiceAbstraction, CryptoServiceAbstraction,
OrganizationUserService, OrganizationUserService,
I18nServiceAbstraction, I18nServiceAbstraction,
@ -964,6 +970,11 @@ import { ModalService } from "./modal.service";
useClass: BadgeSettingsService, useClass: BadgeSettingsService,
deps: [StateProvider], deps: [StateProvider],
}, },
{
provide: DomainSettingsService,
useClass: DefaultDomainSettingsService,
deps: [StateProvider],
},
{ {
provide: BiometricStateService, provide: BiometricStateService,
useClass: DefaultBiometricStateService, useClass: DefaultBiometricStateService,

View File

@ -13,6 +13,7 @@ import { OrganizationUserStatusType, PolicyType } from "@bitwarden/common/admin-
import { Organization } from "@bitwarden/common/admin-console/models/domain/organization"; import { Organization } from "@bitwarden/common/admin-console/models/domain/organization";
import { EventType } from "@bitwarden/common/enums"; import { EventType } from "@bitwarden/common/enums";
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum"; import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
import { UriMatchStrategy } from "@bitwarden/common/models/domain/domain-service";
import { ConfigServiceAbstraction } from "@bitwarden/common/platform/abstractions/config/config.service.abstraction"; import { ConfigServiceAbstraction } from "@bitwarden/common/platform/abstractions/config/config.service.abstraction";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
@ -24,7 +25,7 @@ import { SendApiService } from "@bitwarden/common/tools/send/services/send-api.s
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service"; import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
import { CollectionService } from "@bitwarden/common/vault/abstractions/collection.service"; import { CollectionService } from "@bitwarden/common/vault/abstractions/collection.service";
import { FolderService } from "@bitwarden/common/vault/abstractions/folder/folder.service.abstraction"; import { FolderService } from "@bitwarden/common/vault/abstractions/folder/folder.service.abstraction";
import { CipherType, SecureNoteType, UriMatchType } from "@bitwarden/common/vault/enums"; import { CipherType, SecureNoteType } from "@bitwarden/common/vault/enums";
import { CipherRepromptType } from "@bitwarden/common/vault/enums/cipher-reprompt-type"; import { CipherRepromptType } from "@bitwarden/common/vault/enums/cipher-reprompt-type";
import { Cipher } from "@bitwarden/common/vault/models/domain/cipher"; import { Cipher } from "@bitwarden/common/vault/models/domain/cipher";
import { CardView } from "@bitwarden/common/vault/models/view/card.view"; import { CardView } from "@bitwarden/common/vault/models/view/card.view";
@ -164,12 +165,12 @@ export class AddEditComponent implements OnInit, OnDestroy {
]; ];
this.uriMatchOptions = [ this.uriMatchOptions = [
{ name: i18nService.t("defaultMatchDetection"), value: null }, { name: i18nService.t("defaultMatchDetection"), value: null },
{ name: i18nService.t("baseDomain"), value: UriMatchType.Domain }, { name: i18nService.t("baseDomain"), value: UriMatchStrategy.Domain },
{ name: i18nService.t("host"), value: UriMatchType.Host }, { name: i18nService.t("host"), value: UriMatchStrategy.Host },
{ name: i18nService.t("startsWith"), value: UriMatchType.StartsWith }, { name: i18nService.t("startsWith"), value: UriMatchStrategy.StartsWith },
{ name: i18nService.t("regEx"), value: UriMatchType.RegularExpression }, { name: i18nService.t("regEx"), value: UriMatchStrategy.RegularExpression },
{ name: i18nService.t("exact"), value: UriMatchType.Exact }, { name: i18nService.t("exact"), value: UriMatchStrategy.Exact },
{ name: i18nService.t("never"), value: UriMatchType.Never }, { name: i18nService.t("never"), value: UriMatchStrategy.Never },
]; ];
this.autofillOnPageLoadOptions = [ this.autofillOnPageLoadOptions = [
{ name: i18nService.t("autoFillOnPageLoadUseDefault"), value: null }, { name: i18nService.t("autoFillOnPageLoadUseDefault"), value: null },

View File

@ -10,7 +10,11 @@ module.exports = {
displayName: "libs/auth tests", displayName: "libs/auth tests",
preset: "jest-preset-angular", preset: "jest-preset-angular",
setupFilesAfterEnv: ["<rootDir>/test.setup.ts"], setupFilesAfterEnv: ["<rootDir>/test.setup.ts"],
moduleNameMapper: pathsToModuleNameMapper(compilerOptions?.paths || {}, { moduleNameMapper: pathsToModuleNameMapper(
prefix: "<rootDir>/", // lets us use @bitwarden/common/spec in tests
}), { "@bitwarden/common/spec": ["../common/spec"], ...(compilerOptions?.paths ?? {}) },
{
prefix: "<rootDir>/",
},
),
}; };

View File

@ -1,7 +1,9 @@
import { Observable } from "rxjs"; import { Observable } from "rxjs";
import { AuthenticationType } from "@bitwarden/common/auth/enums/authentication-type";
import { AuthResult } from "@bitwarden/common/auth/models/domain/auth-result"; import { AuthResult } from "@bitwarden/common/auth/models/domain/auth-result";
import { TokenTwoFactorRequest } from "@bitwarden/common/auth/models/request/identity-token/token-two-factor.request"; import { TokenTwoFactorRequest } from "@bitwarden/common/auth/models/request/identity-token/token-two-factor.request";
import { AuthRequestResponse } from "@bitwarden/common/auth/models/response/auth-request.response";
import { AuthRequestPushNotification } from "@bitwarden/common/models/response/notification.response"; import { AuthRequestPushNotification } from "@bitwarden/common/models/response/notification.response";
import { MasterKey } from "@bitwarden/common/types/key"; import { MasterKey } from "@bitwarden/common/types/key";
@ -14,12 +16,45 @@ import {
} from "../models/domain/login-credentials"; } from "../models/domain/login-credentials";
export abstract class LoginStrategyServiceAbstraction { export abstract class LoginStrategyServiceAbstraction {
masterPasswordHash: string; /**
email: string; * The current strategy being used to authenticate.
accessCode: string; * Emits null if the session has timed out.
authRequestId: string; */
ssoEmail2FaSessionToken: string; currentAuthType$: Observable<AuthenticationType | null>;
/**
* Emits when an auth request has been approved.
*/
authRequestPushNotification$: Observable<string>;
/**
* If the login strategy uses the email address of the user, this
* will return it. Otherwise, it will return null.
*/
getEmail: () => Promise<string | null>;
/**
* If the user is logging in with a master password, this will return
* the master password hash. Otherwise, it will return null.
*/
getMasterPasswordHash: () => Promise<string | null>;
/**
* If the user is logging in with SSO, this will return
* the email auth token. Otherwise, it will return null.
* @see {@link SsoLoginStrategyData.ssoEmail2FaSessionToken}
*/
getSsoEmail2FaSessionToken: () => Promise<string | null>;
/**
* Returns the access code if the user is logging in with an
* Auth Request. Otherwise, it will return null.
*/
getAccessCode: () => Promise<string | null>;
/**
* Returns the auth request ID if the user is logging in with an
* Auth Request. Otherwise, it will return null.
*/
getAuthRequestId: () => Promise<string | null>;
/**
* Sends a token request to the server using the provided credentials.
*/
logIn: ( logIn: (
credentials: credentials:
| UserApiLoginCredentials | UserApiLoginCredentials
@ -28,15 +63,30 @@ export abstract class LoginStrategyServiceAbstraction {
| AuthRequestLoginCredentials | AuthRequestLoginCredentials
| WebAuthnLoginCredentials, | WebAuthnLoginCredentials,
) => Promise<AuthResult>; ) => Promise<AuthResult>;
/**
* Sends a token request to the server with the provided two factor token
* and captcha response. This uses data stored from {@link LoginStrategyServiceAbstraction.logIn},
* so that must be called first.
* Returns an error if no session data is found.
*/
logInTwoFactor: ( logInTwoFactor: (
twoFactor: TokenTwoFactorRequest, twoFactor: TokenTwoFactorRequest,
captchaResponse: string, captchaResponse: string,
) => Promise<AuthResult>; ) => Promise<AuthResult>;
/**
* Creates a master key from the provided master password and email.
*/
makePreloginKey: (masterPassword: string, email: string) => Promise<MasterKey>; makePreloginKey: (masterPassword: string, email: string) => Promise<MasterKey>;
authingWithUserApiKey: () => boolean; /**
authingWithSso: () => boolean; * Sends a notification to {@link LoginStrategyServiceAbstraction.authRequestPushNotification}
authingWithPassword: () => boolean; */
authingWithPasswordless: () => boolean; sendAuthRequestPushNotification: (notification: AuthRequestPushNotification) => Promise<void>;
authResponsePushNotification: (notification: AuthRequestPushNotification) => Promise<any>; /**
getPushNotificationObs$: () => Observable<any>; * Sends a response to an auth request.
*/
passwordlessLogin: (
id: string,
key: string,
requestApproved: boolean,
) => Promise<AuthRequestResponse>;
} }

View File

@ -18,10 +18,15 @@ import { MasterKey, UserKey } from "@bitwarden/common/types/key";
import { AuthRequestLoginCredentials } from "../models/domain/login-credentials"; import { AuthRequestLoginCredentials } from "../models/domain/login-credentials";
import { AuthRequestLoginStrategy } from "./auth-request-login.strategy"; import {
AuthRequestLoginStrategy,
AuthRequestLoginStrategyData,
} from "./auth-request-login.strategy";
import { identityTokenResponseFactory } from "./login.strategy.spec"; import { identityTokenResponseFactory } from "./login.strategy.spec";
describe("AuthRequestLoginStrategy", () => { describe("AuthRequestLoginStrategy", () => {
let cache: AuthRequestLoginStrategyData;
let cryptoService: MockProxy<CryptoService>; let cryptoService: MockProxy<CryptoService>;
let apiService: MockProxy<ApiService>; let apiService: MockProxy<ApiService>;
let tokenService: MockProxy<TokenService>; let tokenService: MockProxy<TokenService>;
@ -65,6 +70,7 @@ describe("AuthRequestLoginStrategy", () => {
tokenService.decodeToken.mockResolvedValue({}); tokenService.decodeToken.mockResolvedValue({});
authRequestLoginStrategy = new AuthRequestLoginStrategy( authRequestLoginStrategy = new AuthRequestLoginStrategy(
cache,
cryptoService, cryptoService,
apiService, apiService,
tokenService, tokenService,

View File

@ -1,3 +1,6 @@
import { Observable, map, BehaviorSubject } from "rxjs";
import { Jsonify } from "type-fest";
import { ApiService } from "@bitwarden/common/abstractions/api.service"; import { ApiService } from "@bitwarden/common/abstractions/api.service";
import { DeviceTrustCryptoServiceAbstraction } from "@bitwarden/common/auth/abstractions/device-trust-crypto.service.abstraction"; import { DeviceTrustCryptoServiceAbstraction } from "@bitwarden/common/auth/abstractions/device-trust-crypto.service.abstraction";
import { TokenService } from "@bitwarden/common/auth/abstractions/token.service"; import { TokenService } from "@bitwarden/common/auth/abstractions/token.service";
@ -14,26 +17,33 @@ import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/pl
import { StateService } from "@bitwarden/common/platform/abstractions/state.service"; import { StateService } from "@bitwarden/common/platform/abstractions/state.service";
import { AuthRequestLoginCredentials } from "../models/domain/login-credentials"; import { AuthRequestLoginCredentials } from "../models/domain/login-credentials";
import { CacheData } from "../services/login-strategies/login-strategy.state";
import { LoginStrategy } from "./login.strategy"; import { LoginStrategy, LoginStrategyData } from "./login.strategy";
export class AuthRequestLoginStrategyData implements LoginStrategyData {
tokenRequest: PasswordTokenRequest;
captchaBypassToken: string;
authRequestCredentials: AuthRequestLoginCredentials;
static fromJSON(obj: Jsonify<AuthRequestLoginStrategyData>): AuthRequestLoginStrategyData {
const data = Object.assign(new AuthRequestLoginStrategyData(), obj, {
tokenRequest: PasswordTokenRequest.fromJSON(obj.tokenRequest),
authRequestCredentials: AuthRequestLoginCredentials.fromJSON(obj.authRequestCredentials),
});
return data;
}
}
export class AuthRequestLoginStrategy extends LoginStrategy { export class AuthRequestLoginStrategy extends LoginStrategy {
get email() { email$: Observable<string>;
return this.tokenRequest.email; accessCode$: Observable<string>;
} authRequestId$: Observable<string>;
get accessCode() { protected cache: BehaviorSubject<AuthRequestLoginStrategyData>;
return this.authRequestCredentials.accessCode;
}
get authRequestId() {
return this.authRequestCredentials.authRequestId;
}
tokenRequest: PasswordTokenRequest;
private authRequestCredentials: AuthRequestLoginCredentials;
constructor( constructor(
data: AuthRequestLoginStrategyData,
cryptoService: CryptoService, cryptoService: CryptoService,
apiService: ApiService, apiService: ApiService,
tokenService: TokenService, tokenService: TokenService,
@ -56,22 +66,26 @@ export class AuthRequestLoginStrategy extends LoginStrategy {
stateService, stateService,
twoFactorService, twoFactorService,
); );
this.cache = new BehaviorSubject(data);
this.email$ = this.cache.pipe(map((data) => data.tokenRequest.email));
this.accessCode$ = this.cache.pipe(map((data) => data.authRequestCredentials.accessCode));
this.authRequestId$ = this.cache.pipe(map((data) => data.authRequestCredentials.authRequestId));
} }
override async logIn(credentials: AuthRequestLoginCredentials) { override async logIn(credentials: AuthRequestLoginCredentials) {
// NOTE: To avoid DeadObject references on Firefox, do not set the credentials object directly const data = new AuthRequestLoginStrategyData();
// Use deep copy in future if objects are added that were created in popup data.tokenRequest = new PasswordTokenRequest(
this.authRequestCredentials = { ...credentials };
this.tokenRequest = new PasswordTokenRequest(
credentials.email, credentials.email,
credentials.accessCode, credentials.accessCode,
null, null,
await this.buildTwoFactor(credentials.twoFactor), await this.buildTwoFactor(credentials.twoFactor),
await this.buildDeviceRequest(), await this.buildDeviceRequest(),
); );
data.tokenRequest.setAuthRequestAccessCode(credentials.authRequestId);
data.authRequestCredentials = credentials;
this.cache.next(data);
this.tokenRequest.setAuthRequestAccessCode(credentials.authRequestId);
const [authResult] = await this.startLogIn(); const [authResult] = await this.startLogIn();
return authResult; return authResult;
} }
@ -80,27 +94,32 @@ export class AuthRequestLoginStrategy extends LoginStrategy {
twoFactor: TokenTwoFactorRequest, twoFactor: TokenTwoFactorRequest,
captchaResponse: string, captchaResponse: string,
): Promise<AuthResult> { ): Promise<AuthResult> {
this.tokenRequest.captchaResponse = captchaResponse ?? this.captchaBypassToken; const data = this.cache.value;
data.tokenRequest.captchaResponse = captchaResponse ?? data.captchaBypassToken;
this.cache.next(data);
return super.logInTwoFactor(twoFactor); return super.logInTwoFactor(twoFactor);
} }
protected override async setMasterKey(response: IdentityTokenResponse) { protected override async setMasterKey(response: IdentityTokenResponse) {
const authRequestCredentials = this.cache.value.authRequestCredentials;
if ( if (
this.authRequestCredentials.decryptedMasterKey && authRequestCredentials.decryptedMasterKey &&
this.authRequestCredentials.decryptedMasterKeyHash authRequestCredentials.decryptedMasterKeyHash
) { ) {
await this.cryptoService.setMasterKey(this.authRequestCredentials.decryptedMasterKey); await this.cryptoService.setMasterKey(authRequestCredentials.decryptedMasterKey);
await this.cryptoService.setMasterKeyHash(this.authRequestCredentials.decryptedMasterKeyHash); await this.cryptoService.setMasterKeyHash(authRequestCredentials.decryptedMasterKeyHash);
} }
} }
protected override async setUserKey(response: IdentityTokenResponse): Promise<void> { protected override async setUserKey(response: IdentityTokenResponse): Promise<void> {
const authRequestCredentials = this.cache.value.authRequestCredentials;
// User now may or may not have a master password // User now may or may not have a master password
// but set the master key encrypted user key if it exists regardless // but set the master key encrypted user key if it exists regardless
await this.cryptoService.setMasterKeyEncryptedUserKey(response.key); await this.cryptoService.setMasterKeyEncryptedUserKey(response.key);
if (this.authRequestCredentials.decryptedUserKey) { if (authRequestCredentials.decryptedUserKey) {
await this.cryptoService.setUserKey(this.authRequestCredentials.decryptedUserKey); await this.cryptoService.setUserKey(authRequestCredentials.decryptedUserKey);
} else { } else {
await this.trySetUserKeyWithMasterKey(); await this.trySetUserKeyWithMasterKey();
// Establish trust if required after setting user key // Establish trust if required after setting user key
@ -121,4 +140,10 @@ export class AuthRequestLoginStrategy extends LoginStrategy {
response.privateKey ?? (await this.createKeyPairForOldAccount()), response.privateKey ?? (await this.createKeyPairForOldAccount()),
); );
} }
exportCache(): CacheData {
return {
authRequest: this.cache.value,
};
}
} }

View File

@ -40,7 +40,7 @@ import { UserKey, MasterKey, DeviceKey } from "@bitwarden/common/types/key";
import { LoginStrategyServiceAbstraction } from "../abstractions/login-strategy.service"; import { LoginStrategyServiceAbstraction } from "../abstractions/login-strategy.service";
import { PasswordLoginCredentials } from "../models/domain/login-credentials"; import { PasswordLoginCredentials } from "../models/domain/login-credentials";
import { PasswordLoginStrategy } from "./password-login.strategy"; import { PasswordLoginStrategy, PasswordLoginStrategyData } from "./password-login.strategy";
const email = "hello@world.com"; const email = "hello@world.com";
const masterPassword = "password"; const masterPassword = "password";
@ -94,6 +94,8 @@ export function identityTokenResponseFactory(
// TODO: add tests for latest changes to base class for TDE // TODO: add tests for latest changes to base class for TDE
describe("LoginStrategy", () => { describe("LoginStrategy", () => {
let cache: PasswordLoginStrategyData;
let loginStrategyService: MockProxy<LoginStrategyServiceAbstraction>; let loginStrategyService: MockProxy<LoginStrategyServiceAbstraction>;
let cryptoService: MockProxy<CryptoService>; let cryptoService: MockProxy<CryptoService>;
let apiService: MockProxy<ApiService>; let apiService: MockProxy<ApiService>;
@ -129,6 +131,7 @@ describe("LoginStrategy", () => {
// The base class is abstract so we test it via PasswordLoginStrategy // The base class is abstract so we test it via PasswordLoginStrategy
passwordLoginStrategy = new PasswordLoginStrategy( passwordLoginStrategy = new PasswordLoginStrategy(
cache,
cryptoService, cryptoService,
apiService, apiService,
tokenService, tokenService,
@ -377,11 +380,23 @@ describe("LoginStrategy", () => {
it("sends 2FA token provided by user to server (two-step)", async () => { it("sends 2FA token provided by user to server (two-step)", async () => {
// Simulate a partially completed login // Simulate a partially completed login
passwordLoginStrategy.tokenRequest = new PasswordTokenRequest( cache = new PasswordLoginStrategyData();
email, cache.tokenRequest = new PasswordTokenRequest(email, masterPasswordHash, null, null);
masterPasswordHash,
null, passwordLoginStrategy = new PasswordLoginStrategy(
null, cache,
cryptoService,
apiService,
tokenService,
appIdService,
platformUtilsService,
messagingService,
logService,
stateService,
twoFactorService,
passwordStrengthService,
policyService,
loginStrategyService,
); );
apiService.postIdentityToken.mockResolvedValue(identityTokenResponseFactory()); apiService.postIdentityToken.mockResolvedValue(identityTokenResponseFactory());

View File

@ -1,3 +1,5 @@
import { BehaviorSubject } from "rxjs";
import { ApiService } from "@bitwarden/common/abstractions/api.service"; import { ApiService } from "@bitwarden/common/abstractions/api.service";
import { TokenService } from "@bitwarden/common/auth/abstractions/token.service"; import { TokenService } from "@bitwarden/common/auth/abstractions/token.service";
import { TwoFactorService } from "@bitwarden/common/auth/abstractions/two-factor.service"; import { TwoFactorService } from "@bitwarden/common/auth/abstractions/two-factor.service";
@ -36,16 +38,21 @@ import {
AuthRequestLoginCredentials, AuthRequestLoginCredentials,
WebAuthnLoginCredentials, WebAuthnLoginCredentials,
} from "../models/domain/login-credentials"; } from "../models/domain/login-credentials";
import { CacheData } from "../services/login-strategies/login-strategy.state";
type IdentityResponse = IdentityTokenResponse | IdentityTwoFactorResponse | IdentityCaptchaResponse; type IdentityResponse = IdentityTokenResponse | IdentityTwoFactorResponse | IdentityCaptchaResponse;
export abstract class LoginStrategy { export abstract class LoginStrategyData {
protected abstract tokenRequest: tokenRequest:
| UserApiTokenRequest | UserApiTokenRequest
| PasswordTokenRequest | PasswordTokenRequest
| SsoTokenRequest | SsoTokenRequest
| WebAuthnLoginTokenRequest; | WebAuthnLoginTokenRequest;
protected captchaBypassToken: string = null; captchaBypassToken?: string;
}
export abstract class LoginStrategy {
protected abstract cache: BehaviorSubject<LoginStrategyData>;
constructor( constructor(
protected cryptoService: CryptoService, protected cryptoService: CryptoService,
@ -59,6 +66,8 @@ export abstract class LoginStrategy {
protected twoFactorService: TwoFactorService, protected twoFactorService: TwoFactorService,
) {} ) {}
abstract exportCache(): CacheData;
abstract logIn( abstract logIn(
credentials: credentials:
| UserApiLoginCredentials | UserApiLoginCredentials
@ -72,7 +81,9 @@ export abstract class LoginStrategy {
twoFactor: TokenTwoFactorRequest, twoFactor: TokenTwoFactorRequest,
captchaResponse: string = null, captchaResponse: string = null,
): Promise<AuthResult> { ): Promise<AuthResult> {
this.tokenRequest.setTwoFactor(twoFactor); const data = this.cache.value;
data.tokenRequest.setTwoFactor(twoFactor);
this.cache.next(data);
const [authResult] = await this.startLogIn(); const [authResult] = await this.startLogIn();
return authResult; return authResult;
} }
@ -80,7 +91,8 @@ export abstract class LoginStrategy {
protected async startLogIn(): Promise<[AuthResult, IdentityResponse]> { protected async startLogIn(): Promise<[AuthResult, IdentityResponse]> {
this.twoFactorService.clearSelectedProvider(); this.twoFactorService.clearSelectedProvider();
const response = await this.apiService.postIdentityToken(this.tokenRequest); const tokenRequest = this.cache.value.tokenRequest;
const response = await this.apiService.postIdentityToken(tokenRequest);
if (response instanceof IdentityTwoFactorResponse) { if (response instanceof IdentityTwoFactorResponse) {
return [await this.processTwoFactorResponse(response), response]; return [await this.processTwoFactorResponse(response), response];
@ -195,9 +207,7 @@ export abstract class LoginStrategy {
// The keys comes from different sources depending on the login strategy // The keys comes from different sources depending on the login strategy
protected abstract setMasterKey(response: IdentityTokenResponse): Promise<void>; protected abstract setMasterKey(response: IdentityTokenResponse): Promise<void>;
protected abstract setUserKey(response: IdentityTokenResponse): Promise<void>; protected abstract setUserKey(response: IdentityTokenResponse): Promise<void>;
protected abstract setPrivateKey(response: IdentityTokenResponse): Promise<void>; protected abstract setPrivateKey(response: IdentityTokenResponse): Promise<void>;
// Old accounts used master key for encryption. We are forcing migrations but only need to // Old accounts used master key for encryption. We are forcing migrations but only need to
@ -221,7 +231,7 @@ export abstract class LoginStrategy {
result.twoFactorProviders = response.twoFactorProviders2; result.twoFactorProviders = response.twoFactorProviders2;
this.twoFactorService.setProviders(response); this.twoFactorService.setProviders(response);
this.captchaBypassToken = response.captchaToken ?? null; this.cache.next({ ...this.cache.value, captchaBypassToken: response.captchaToken ?? null });
result.ssoEmail2FaSessionToken = response.ssoEmail2faSessionToken; result.ssoEmail2FaSessionToken = response.ssoEmail2faSessionToken;
result.email = response.email; result.email = response.email;
return result; return result;

View File

@ -29,7 +29,7 @@ import { LoginStrategyServiceAbstraction } from "../abstractions";
import { PasswordLoginCredentials } from "../models/domain/login-credentials"; import { PasswordLoginCredentials } from "../models/domain/login-credentials";
import { identityTokenResponseFactory } from "./login.strategy.spec"; import { identityTokenResponseFactory } from "./login.strategy.spec";
import { PasswordLoginStrategy } from "./password-login.strategy"; import { PasswordLoginStrategy, PasswordLoginStrategyData } from "./password-login.strategy";
const email = "hello@world.com"; const email = "hello@world.com";
const masterPassword = "password"; const masterPassword = "password";
@ -47,6 +47,8 @@ const masterPasswordPolicy = new MasterPasswordPolicyResponse({
}); });
describe("PasswordLoginStrategy", () => { describe("PasswordLoginStrategy", () => {
let cache: PasswordLoginStrategyData;
let loginStrategyService: MockProxy<LoginStrategyServiceAbstraction>; let loginStrategyService: MockProxy<LoginStrategyServiceAbstraction>;
let cryptoService: MockProxy<CryptoService>; let cryptoService: MockProxy<CryptoService>;
let apiService: MockProxy<ApiService>; let apiService: MockProxy<ApiService>;
@ -93,6 +95,7 @@ describe("PasswordLoginStrategy", () => {
policyService.evaluateMasterPassword.mockReturnValue(true); policyService.evaluateMasterPassword.mockReturnValue(true);
passwordLoginStrategy = new PasswordLoginStrategy( passwordLoginStrategy = new PasswordLoginStrategy(
cache,
cryptoService, cryptoService,
apiService, apiService,
tokenService, tokenService,

View File

@ -1,3 +1,6 @@
import { BehaviorSubject, map, Observable } from "rxjs";
import { Jsonify } from "type-fest";
import { ApiService } from "@bitwarden/common/abstractions/api.service"; import { ApiService } from "@bitwarden/common/abstractions/api.service";
import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction"; import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction";
import { MasterPasswordPolicyOptions } from "@bitwarden/common/admin-console/models/domain/master-password-policy-options"; import { MasterPasswordPolicyOptions } from "@bitwarden/common/admin-console/models/domain/master-password-policy-options";
@ -17,35 +20,56 @@ import { MessagingService } from "@bitwarden/common/platform/abstractions/messag
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
import { StateService } from "@bitwarden/common/platform/abstractions/state.service"; import { StateService } from "@bitwarden/common/platform/abstractions/state.service";
import { HashPurpose } from "@bitwarden/common/platform/enums"; import { HashPurpose } from "@bitwarden/common/platform/enums";
import { SymmetricCryptoKey } from "@bitwarden/common/platform/models/domain/symmetric-crypto-key";
import { PasswordStrengthServiceAbstraction } from "@bitwarden/common/tools/password-strength"; import { PasswordStrengthServiceAbstraction } from "@bitwarden/common/tools/password-strength";
import { MasterKey } from "@bitwarden/common/types/key"; import { MasterKey } from "@bitwarden/common/types/key";
import { LoginStrategyServiceAbstraction } from "../abstractions"; import { LoginStrategyServiceAbstraction } from "../abstractions";
import { PasswordLoginCredentials } from "../models/domain/login-credentials"; import { PasswordLoginCredentials } from "../models/domain/login-credentials";
import { CacheData } from "../services/login-strategies/login-strategy.state";
import { LoginStrategy } from "./login.strategy"; import { LoginStrategy, LoginStrategyData } from "./login.strategy";
export class PasswordLoginStrategyData implements LoginStrategyData {
tokenRequest: PasswordTokenRequest;
captchaBypassToken?: string;
/**
* The local version of the user's master key hash
*/
localMasterKeyHash: string;
/**
* The user's master key
*/
masterKey: MasterKey;
/**
* Tracks if the user needs to update their password due to
* a password that does not meet an organization's master password policy.
*/
forcePasswordResetReason: ForceSetPasswordReason = ForceSetPasswordReason.None;
static fromJSON(obj: Jsonify<PasswordLoginStrategyData>): PasswordLoginStrategyData {
const data = Object.assign(new PasswordLoginStrategyData(), obj, {
tokenRequest: PasswordTokenRequest.fromJSON(obj.tokenRequest),
masterKey: SymmetricCryptoKey.fromJSON(obj.masterKey),
});
return data;
}
}
export class PasswordLoginStrategy extends LoginStrategy { export class PasswordLoginStrategy extends LoginStrategy {
get email() {
return this.tokenRequest.email;
}
get masterPasswordHash() {
return this.tokenRequest.masterPasswordHash;
}
tokenRequest: PasswordTokenRequest;
private localMasterKeyHash: string;
private masterKey: MasterKey;
/** /**
* Options to track if the user needs to update their password due to a password that does not meet an organization's * The email address of the user attempting to log in.
* master password policy.
*/ */
private forcePasswordResetReason: ForceSetPasswordReason = ForceSetPasswordReason.None; email$: Observable<string>;
/**
* The master key hash of the user attempting to log in.
*/
masterKeyHash$: Observable<string | null>;
protected cache: BehaviorSubject<PasswordLoginStrategyData>;
constructor( constructor(
data: PasswordLoginStrategyData,
cryptoService: CryptoService, cryptoService: CryptoService,
apiService: ApiService, apiService: ApiService,
tokenService: TokenService, tokenService: TokenService,
@ -70,42 +94,27 @@ export class PasswordLoginStrategy extends LoginStrategy {
stateService, stateService,
twoFactorService, twoFactorService,
); );
}
override async logInTwoFactor( this.cache = new BehaviorSubject(data);
twoFactor: TokenTwoFactorRequest, this.email$ = this.cache.pipe(map((state) => state.tokenRequest.email));
captchaResponse: string, this.masterKeyHash$ = this.cache.pipe(map((state) => state.localMasterKeyHash));
): Promise<AuthResult> {
this.tokenRequest.captchaResponse = captchaResponse ?? this.captchaBypassToken;
const result = await super.logInTwoFactor(twoFactor);
// 2FA was successful, save the force update password options with the state service if defined
if (
!result.requiresTwoFactor &&
!result.requiresCaptcha &&
this.forcePasswordResetReason != ForceSetPasswordReason.None
) {
await this.stateService.setForceSetPasswordReason(this.forcePasswordResetReason);
result.forcePasswordReset = this.forcePasswordResetReason;
}
return result;
} }
override async logIn(credentials: PasswordLoginCredentials) { override async logIn(credentials: PasswordLoginCredentials) {
const { email, masterPassword, captchaToken, twoFactor } = credentials; const { email, masterPassword, captchaToken, twoFactor } = credentials;
this.masterKey = await this.loginStrategyService.makePreloginKey(masterPassword, email); const data = new PasswordLoginStrategyData();
data.masterKey = await this.loginStrategyService.makePreloginKey(masterPassword, email);
// Hash the password early (before authentication) so we don't persist it in memory in plaintext // Hash the password early (before authentication) so we don't persist it in memory in plaintext
this.localMasterKeyHash = await this.cryptoService.hashMasterKey( data.localMasterKeyHash = await this.cryptoService.hashMasterKey(
masterPassword, masterPassword,
this.masterKey, data.masterKey,
HashPurpose.LocalAuthorization, HashPurpose.LocalAuthorization,
); );
const masterKeyHash = await this.cryptoService.hashMasterKey(masterPassword, this.masterKey); const masterKeyHash = await this.cryptoService.hashMasterKey(masterPassword, data.masterKey);
this.tokenRequest = new PasswordTokenRequest( data.tokenRequest = new PasswordTokenRequest(
email, email,
masterKeyHash, masterKeyHash,
captchaToken, captchaToken,
@ -113,6 +122,8 @@ export class PasswordLoginStrategy extends LoginStrategy {
await this.buildDeviceRequest(), await this.buildDeviceRequest(),
); );
this.cache.next(data);
const [authResult, identityResponse] = await this.startLogIn(); const [authResult, identityResponse] = await this.startLogIn();
const masterPasswordPolicyOptions = const masterPasswordPolicyOptions =
@ -129,7 +140,10 @@ export class PasswordLoginStrategy extends LoginStrategy {
if (!meetsRequirements) { if (!meetsRequirements) {
if (authResult.requiresCaptcha || authResult.requiresTwoFactor) { if (authResult.requiresCaptcha || authResult.requiresTwoFactor) {
// Save the flag to this strategy for later use as the master password is about to pass out of scope // Save the flag to this strategy for later use as the master password is about to pass out of scope
this.forcePasswordResetReason = ForceSetPasswordReason.WeakMasterPassword; this.cache.next({
...this.cache.value,
forcePasswordResetReason: ForceSetPasswordReason.WeakMasterPassword,
});
} else { } else {
// Authentication was successful, save the force update password options with the state service // Authentication was successful, save the force update password options with the state service
await this.stateService.setForceSetPasswordReason( await this.stateService.setForceSetPasswordReason(
@ -142,9 +156,34 @@ export class PasswordLoginStrategy extends LoginStrategy {
return authResult; return authResult;
} }
override async logInTwoFactor(
twoFactor: TokenTwoFactorRequest,
captchaResponse: string,
): Promise<AuthResult> {
this.cache.next({
...this.cache.value,
captchaBypassToken: captchaResponse ?? this.cache.value.captchaBypassToken,
});
const result = await super.logInTwoFactor(twoFactor);
// 2FA was successful, save the force update password options with the state service if defined
const forcePasswordResetReason = this.cache.value.forcePasswordResetReason;
if (
!result.requiresTwoFactor &&
!result.requiresCaptcha &&
forcePasswordResetReason != ForceSetPasswordReason.None
) {
await this.stateService.setForceSetPasswordReason(forcePasswordResetReason);
result.forcePasswordReset = forcePasswordResetReason;
}
return result;
}
protected override async setMasterKey(response: IdentityTokenResponse) { protected override async setMasterKey(response: IdentityTokenResponse) {
await this.cryptoService.setMasterKey(this.masterKey); const { masterKey, localMasterKeyHash } = this.cache.value;
await this.cryptoService.setMasterKeyHash(this.localMasterKeyHash); await this.cryptoService.setMasterKey(masterKey);
await this.cryptoService.setMasterKeyHash(localMasterKeyHash);
} }
protected override async setUserKey(response: IdentityTokenResponse): Promise<void> { protected override async setUserKey(response: IdentityTokenResponse): Promise<void> {
@ -191,4 +230,10 @@ export class PasswordLoginStrategy extends LoginStrategy {
return this.policyService.evaluateMasterPassword(passwordStrength, masterPassword, options); return this.policyService.evaluateMasterPassword(passwordStrength, masterPassword, options);
} }
exportCache(): CacheData {
return {
password: this.cache.value,
};
}
} }

View File

@ -5,8 +5,11 @@ import { DeviceTrustCryptoServiceAbstraction } from "@bitwarden/common/auth/abst
import { KeyConnectorService } from "@bitwarden/common/auth/abstractions/key-connector.service"; import { KeyConnectorService } from "@bitwarden/common/auth/abstractions/key-connector.service";
import { TokenService } from "@bitwarden/common/auth/abstractions/token.service"; import { TokenService } from "@bitwarden/common/auth/abstractions/token.service";
import { TwoFactorService } from "@bitwarden/common/auth/abstractions/two-factor.service"; import { TwoFactorService } from "@bitwarden/common/auth/abstractions/two-factor.service";
import { AdminAuthRequestStorable } from "@bitwarden/common/auth/models/domain/admin-auth-req-storable";
import { AuthRequestResponse } from "@bitwarden/common/auth/models/response/auth-request.response";
import { IdentityTokenResponse } from "@bitwarden/common/auth/models/response/identity-token.response"; import { IdentityTokenResponse } from "@bitwarden/common/auth/models/response/identity-token.response";
import { IUserDecryptionOptionsServerResponse } from "@bitwarden/common/auth/models/response/user-decryption-options/user-decryption-options.response"; import { IUserDecryptionOptionsServerResponse } from "@bitwarden/common/auth/models/response/user-decryption-options/user-decryption-options.response";
import { ErrorResponse } from "@bitwarden/common/models/response/error.response";
import { AppIdService } from "@bitwarden/common/platform/abstractions/app-id.service"; import { AppIdService } from "@bitwarden/common/platform/abstractions/app-id.service";
import { CryptoService } from "@bitwarden/common/platform/abstractions/crypto.service"; import { CryptoService } from "@bitwarden/common/platform/abstractions/crypto.service";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
@ -25,9 +28,6 @@ import { SsoLoginCredentials } from "../models/domain/login-credentials";
import { identityTokenResponseFactory } from "./login.strategy.spec"; import { identityTokenResponseFactory } from "./login.strategy.spec";
import { SsoLoginStrategy } from "./sso-login.strategy"; import { SsoLoginStrategy } from "./sso-login.strategy";
// TODO: Add tests for new trySetUserKeyWithApprovedAdminRequestIfExists logic
// https://bitwarden.atlassian.net/browse/PM-3339
describe("SsoLoginStrategy", () => { describe("SsoLoginStrategy", () => {
let cryptoService: MockProxy<CryptoService>; let cryptoService: MockProxy<CryptoService>;
let apiService: MockProxy<ApiService>; let apiService: MockProxy<ApiService>;
@ -74,6 +74,7 @@ describe("SsoLoginStrategy", () => {
tokenService.decodeToken.mockResolvedValue({}); tokenService.decodeToken.mockResolvedValue({});
ssoLoginStrategy = new SsoLoginStrategy( ssoLoginStrategy = new SsoLoginStrategy(
null,
cryptoService, cryptoService,
apiService, apiService,
tokenService, tokenService,
@ -258,6 +259,114 @@ describe("SsoLoginStrategy", () => {
// Assert // Assert
expect(cryptoService.setUserKey).not.toHaveBeenCalled(); expect(cryptoService.setUserKey).not.toHaveBeenCalled();
}); });
describe("AdminAuthRequest", () => {
let tokenResponse: IdentityTokenResponse;
beforeEach(() => {
tokenResponse = identityTokenResponseFactory(null, {
HasMasterPassword: true,
TrustedDeviceOption: {
HasAdminApproval: true,
HasLoginApprovingDevice: false,
HasManageResetPasswordPermission: false,
EncryptedPrivateKey: mockEncDevicePrivateKey,
EncryptedUserKey: mockEncUserKey,
},
});
const adminAuthRequest = {
id: "1",
privateKey: "PRIVATE" as any,
} as AdminAuthRequestStorable;
stateService.getAdminAuthRequest.mockResolvedValue(
new AdminAuthRequestStorable(adminAuthRequest),
);
});
it("sets the user key using master key and hash from approved admin request if exists", async () => {
apiService.postIdentityToken.mockResolvedValue(tokenResponse);
cryptoService.hasUserKey.mockResolvedValue(true);
const adminAuthResponse = {
id: "1",
publicKey: "PRIVATE" as any,
key: "KEY" as any,
masterPasswordHash: "HASH" as any,
requestApproved: true,
};
apiService.getAuthRequest.mockResolvedValue(adminAuthResponse as AuthRequestResponse);
await ssoLoginStrategy.logIn(credentials);
expect(authRequestService.setKeysAfterDecryptingSharedMasterKeyAndHash).toHaveBeenCalled();
expect(deviceTrustCryptoService.decryptUserKeyWithDeviceKey).not.toHaveBeenCalled();
});
it("sets the user key from approved admin request if exists", async () => {
apiService.postIdentityToken.mockResolvedValue(tokenResponse);
cryptoService.hasUserKey.mockResolvedValue(true);
const adminAuthResponse = {
id: "1",
publicKey: "PRIVATE" as any,
key: "KEY" as any,
requestApproved: true,
};
apiService.getAuthRequest.mockResolvedValue(adminAuthResponse as AuthRequestResponse);
await ssoLoginStrategy.logIn(credentials);
expect(authRequestService.setUserKeyAfterDecryptingSharedUserKey).toHaveBeenCalled();
expect(deviceTrustCryptoService.decryptUserKeyWithDeviceKey).not.toHaveBeenCalled();
});
it("attempts to establish a trusted device if successful", async () => {
apiService.postIdentityToken.mockResolvedValue(tokenResponse);
cryptoService.hasUserKey.mockResolvedValue(true);
const adminAuthResponse = {
id: "1",
publicKey: "PRIVATE" as any,
key: "KEY" as any,
requestApproved: true,
};
apiService.getAuthRequest.mockResolvedValue(adminAuthResponse as AuthRequestResponse);
await ssoLoginStrategy.logIn(credentials);
expect(authRequestService.setUserKeyAfterDecryptingSharedUserKey).toHaveBeenCalled();
expect(deviceTrustCryptoService.trustDeviceIfRequired).toHaveBeenCalled();
});
it("clears the admin auth request if server returns a 404, meaning it was deleted", async () => {
apiService.postIdentityToken.mockResolvedValue(tokenResponse);
apiService.getAuthRequest.mockRejectedValue(new ErrorResponse(null, 404));
await ssoLoginStrategy.logIn(credentials);
expect(stateService.setAdminAuthRequest).toHaveBeenCalledWith(null);
expect(
authRequestService.setKeysAfterDecryptingSharedMasterKeyAndHash,
).not.toHaveBeenCalled();
expect(authRequestService.setUserKeyAfterDecryptingSharedUserKey).not.toHaveBeenCalled();
expect(deviceTrustCryptoService.trustDeviceIfRequired).not.toHaveBeenCalled();
});
it("attempts to login with a trusted device if admin auth request isn't successful", async () => {
apiService.postIdentityToken.mockResolvedValue(tokenResponse);
const adminAuthResponse = {
id: "1",
publicKey: "PRIVATE" as any,
key: "KEY" as any,
requestApproved: true,
};
apiService.getAuthRequest.mockResolvedValue(adminAuthResponse as AuthRequestResponse);
cryptoService.hasUserKey.mockResolvedValue(false);
deviceTrustCryptoService.getDeviceKey.mockResolvedValue("DEVICE_KEY" as any);
await ssoLoginStrategy.logIn(credentials);
expect(deviceTrustCryptoService.decryptUserKeyWithDeviceKey).toHaveBeenCalled();
});
});
}); });
describe("Key Connector", () => { describe("Key Connector", () => {

View File

@ -1,3 +1,6 @@
import { Observable, map, BehaviorSubject } from "rxjs";
import { Jsonify } from "type-fest";
import { ApiService } from "@bitwarden/common/abstractions/api.service"; import { ApiService } from "@bitwarden/common/abstractions/api.service";
import { DeviceTrustCryptoServiceAbstraction } from "@bitwarden/common/auth/abstractions/device-trust-crypto.service.abstraction"; import { DeviceTrustCryptoServiceAbstraction } from "@bitwarden/common/auth/abstractions/device-trust-crypto.service.abstraction";
import { KeyConnectorService } from "@bitwarden/common/auth/abstractions/key-connector.service"; import { KeyConnectorService } from "@bitwarden/common/auth/abstractions/key-connector.service";
@ -19,20 +22,54 @@ import { StateService } from "@bitwarden/common/platform/abstractions/state.serv
import { AuthRequestServiceAbstraction } from "../abstractions"; import { AuthRequestServiceAbstraction } from "../abstractions";
import { SsoLoginCredentials } from "../models/domain/login-credentials"; import { SsoLoginCredentials } from "../models/domain/login-credentials";
import { CacheData } from "../services/login-strategies/login-strategy.state";
import { LoginStrategy } from "./login.strategy"; import { LoginStrategyData, LoginStrategy } from "./login.strategy";
export class SsoLoginStrategyData implements LoginStrategyData {
captchaBypassToken: string;
tokenRequest: SsoTokenRequest;
/**
* User email address. Only available after authentication.
*/
email?: string;
/**
* The organization ID that the user is logging into. Used for Key Connector
* purposes after authentication.
*/
orgId: string;
/**
* A token provided by the server as an authentication factor for sending
* email OTPs to the user's configured 2FA email address. This is required
* as we don't have a master password hash or other verifiable secret when using SSO.
*/
ssoEmail2FaSessionToken?: string;
static fromJSON(obj: Jsonify<SsoLoginStrategyData>): SsoLoginStrategyData {
return Object.assign(new SsoLoginStrategyData(), obj, {
tokenRequest: SsoTokenRequest.fromJSON(obj.tokenRequest),
});
}
}
export class SsoLoginStrategy extends LoginStrategy { export class SsoLoginStrategy extends LoginStrategy {
tokenRequest: SsoTokenRequest; /**
orgId: string; * @see {@link SsoLoginStrategyData.email}
*/
email$: Observable<string | null>;
/**
* @see {@link SsoLoginStrategyData.orgId}
*/
orgId$: Observable<string>;
/**
* @see {@link SsoLoginStrategyData.ssoEmail2FaSessionToken}
*/
ssoEmail2FaSessionToken$: Observable<string | null>;
// A session token server side to serve as an authentication factor for the user protected cache: BehaviorSubject<SsoLoginStrategyData>;
// in order to send email OTPs to the user's configured 2FA email address
// as we don't have a master password hash or other verifiable secret when using SSO.
ssoEmail2FaSessionToken?: string;
email?: string; // email not preserved through SSO process so get from server
constructor( constructor(
data: SsoLoginStrategyData,
cryptoService: CryptoService, cryptoService: CryptoService,
apiService: ApiService, apiService: ApiService,
tokenService: TokenService, tokenService: TokenService,
@ -58,11 +95,17 @@ export class SsoLoginStrategy extends LoginStrategy {
stateService, stateService,
twoFactorService, twoFactorService,
); );
this.cache = new BehaviorSubject(data);
this.email$ = this.cache.pipe(map((state) => state.email));
this.orgId$ = this.cache.pipe(map((state) => state.orgId));
this.ssoEmail2FaSessionToken$ = this.cache.pipe(map((state) => state.ssoEmail2FaSessionToken));
} }
async logIn(credentials: SsoLoginCredentials) { async logIn(credentials: SsoLoginCredentials) {
this.orgId = credentials.orgId; const data = new SsoLoginStrategyData();
this.tokenRequest = new SsoTokenRequest( data.orgId = credentials.orgId;
data.tokenRequest = new SsoTokenRequest(
credentials.code, credentials.code,
credentials.codeVerifier, credentials.codeVerifier,
credentials.redirectUrl, credentials.redirectUrl,
@ -70,16 +113,24 @@ export class SsoLoginStrategy extends LoginStrategy {
await this.buildDeviceRequest(), await this.buildDeviceRequest(),
); );
this.cache.next(data);
const [ssoAuthResult] = await this.startLogIn(); const [ssoAuthResult] = await this.startLogIn();
this.email = ssoAuthResult.email; const email = ssoAuthResult.email;
this.ssoEmail2FaSessionToken = ssoAuthResult.ssoEmail2FaSessionToken; const ssoEmail2FaSessionToken = ssoAuthResult.ssoEmail2FaSessionToken;
// Auth guard currently handles redirects for this. // Auth guard currently handles redirects for this.
if (ssoAuthResult.forcePasswordReset == ForceSetPasswordReason.AdminForcePasswordReset) { if (ssoAuthResult.forcePasswordReset == ForceSetPasswordReason.AdminForcePasswordReset) {
await this.stateService.setForceSetPasswordReason(ssoAuthResult.forcePasswordReset); await this.stateService.setForceSetPasswordReason(ssoAuthResult.forcePasswordReset);
} }
this.cache.next({
...this.cache.value,
email,
ssoEmail2FaSessionToken,
});
return ssoAuthResult; return ssoAuthResult;
} }
@ -92,7 +143,10 @@ export class SsoLoginStrategy extends LoginStrategy {
// The presence of a masterKeyEncryptedUserKey indicates that the user has already been provisioned in Key Connector. // The presence of a masterKeyEncryptedUserKey indicates that the user has already been provisioned in Key Connector.
const newSsoUser = tokenResponse.key == null; const newSsoUser = tokenResponse.key == null;
if (newSsoUser) { if (newSsoUser) {
await this.keyConnectorService.convertNewSsoUserToKeyConnector(tokenResponse, this.orgId); await this.keyConnectorService.convertNewSsoUserToKeyConnector(
tokenResponse,
this.cache.value.orgId,
);
} else { } else {
const keyConnectorUrl = this.getKeyConnectorUrl(tokenResponse); const keyConnectorUrl = this.getKeyConnectorUrl(tokenResponse);
await this.keyConnectorService.setMasterKeyFromUrl(keyConnectorUrl); await this.keyConnectorService.setMasterKeyFromUrl(keyConnectorUrl);
@ -272,4 +326,10 @@ export class SsoLoginStrategy extends LoginStrategy {
); );
} }
} }
exportCache(): CacheData {
return {
sso: this.cache.value,
};
}
} }

View File

@ -19,9 +19,11 @@ import { UserKey, MasterKey } from "@bitwarden/common/types/key";
import { UserApiLoginCredentials } from "../models/domain/login-credentials"; import { UserApiLoginCredentials } from "../models/domain/login-credentials";
import { identityTokenResponseFactory } from "./login.strategy.spec"; import { identityTokenResponseFactory } from "./login.strategy.spec";
import { UserApiLoginStrategy } from "./user-api-login.strategy"; import { UserApiLoginStrategy, UserApiLoginStrategyData } from "./user-api-login.strategy";
describe("UserApiLoginStrategy", () => { describe("UserApiLoginStrategy", () => {
let cache: UserApiLoginStrategyData;
let cryptoService: MockProxy<CryptoService>; let cryptoService: MockProxy<CryptoService>;
let apiService: MockProxy<ApiService>; let apiService: MockProxy<ApiService>;
let tokenService: MockProxy<TokenService>; let tokenService: MockProxy<TokenService>;
@ -60,6 +62,7 @@ describe("UserApiLoginStrategy", () => {
tokenService.decodeToken.mockResolvedValue({}); tokenService.decodeToken.mockResolvedValue({});
apiLogInStrategy = new UserApiLoginStrategy( apiLogInStrategy = new UserApiLoginStrategy(
cache,
cryptoService, cryptoService,
apiService, apiService,
tokenService, tokenService,

View File

@ -1,3 +1,6 @@
import { BehaviorSubject } from "rxjs";
import { Jsonify } from "type-fest";
import { ApiService } from "@bitwarden/common/abstractions/api.service"; import { ApiService } from "@bitwarden/common/abstractions/api.service";
import { KeyConnectorService } from "@bitwarden/common/auth/abstractions/key-connector.service"; import { KeyConnectorService } from "@bitwarden/common/auth/abstractions/key-connector.service";
import { TokenService } from "@bitwarden/common/auth/abstractions/token.service"; import { TokenService } from "@bitwarden/common/auth/abstractions/token.service";
@ -13,13 +16,26 @@ import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/pl
import { StateService } from "@bitwarden/common/platform/abstractions/state.service"; import { StateService } from "@bitwarden/common/platform/abstractions/state.service";
import { UserApiLoginCredentials } from "../models/domain/login-credentials"; import { UserApiLoginCredentials } from "../models/domain/login-credentials";
import { CacheData } from "../services/login-strategies/login-strategy.state";
import { LoginStrategy } from "./login.strategy"; import { LoginStrategy, LoginStrategyData } from "./login.strategy";
export class UserApiLoginStrategyData implements LoginStrategyData {
tokenRequest: UserApiTokenRequest;
captchaBypassToken: string;
static fromJSON(obj: Jsonify<UserApiLoginStrategyData>): UserApiLoginStrategyData {
return Object.assign(new UserApiLoginStrategyData(), obj, {
tokenRequest: UserApiTokenRequest.fromJSON(obj.tokenRequest),
});
}
}
export class UserApiLoginStrategy extends LoginStrategy { export class UserApiLoginStrategy extends LoginStrategy {
tokenRequest: UserApiTokenRequest; protected cache: BehaviorSubject<UserApiLoginStrategyData>;
constructor( constructor(
data: UserApiLoginStrategyData,
cryptoService: CryptoService, cryptoService: CryptoService,
apiService: ApiService, apiService: ApiService,
tokenService: TokenService, tokenService: TokenService,
@ -43,15 +59,18 @@ export class UserApiLoginStrategy extends LoginStrategy {
stateService, stateService,
twoFactorService, twoFactorService,
); );
this.cache = new BehaviorSubject(data);
} }
override async logIn(credentials: UserApiLoginCredentials) { override async logIn(credentials: UserApiLoginCredentials) {
this.tokenRequest = new UserApiTokenRequest( const data = new UserApiLoginStrategyData();
data.tokenRequest = new UserApiTokenRequest(
credentials.clientId, credentials.clientId,
credentials.clientSecret, credentials.clientSecret,
await this.buildTwoFactor(), await this.buildTwoFactor(),
await this.buildDeviceRequest(), await this.buildDeviceRequest(),
); );
this.cache.next(data);
const [authResult] = await this.startLogIn(); const [authResult] = await this.startLogIn();
return authResult; return authResult;
@ -84,7 +103,15 @@ export class UserApiLoginStrategy extends LoginStrategy {
protected async saveAccountInformation(tokenResponse: IdentityTokenResponse) { protected async saveAccountInformation(tokenResponse: IdentityTokenResponse) {
await super.saveAccountInformation(tokenResponse); await super.saveAccountInformation(tokenResponse);
await this.stateService.setApiKeyClientId(this.tokenRequest.clientId);
await this.stateService.setApiKeyClientSecret(this.tokenRequest.clientSecret); const tokenRequest = this.cache.value.tokenRequest;
await this.stateService.setApiKeyClientId(tokenRequest.clientId);
await this.stateService.setApiKeyClientSecret(tokenRequest.clientSecret);
}
exportCache(): CacheData {
return {
userApiKey: this.cache.value,
};
} }
} }

View File

@ -20,9 +20,11 @@ import { PrfKey, UserKey } from "@bitwarden/common/types/key";
import { WebAuthnLoginCredentials } from "../models/domain/login-credentials"; import { WebAuthnLoginCredentials } from "../models/domain/login-credentials";
import { identityTokenResponseFactory } from "./login.strategy.spec"; import { identityTokenResponseFactory } from "./login.strategy.spec";
import { WebAuthnLoginStrategy } from "./webauthn-login.strategy"; import { WebAuthnLoginStrategy, WebAuthnLoginStrategyData } from "./webauthn-login.strategy";
describe("WebAuthnLoginStrategy", () => { describe("WebAuthnLoginStrategy", () => {
let cache: WebAuthnLoginStrategyData;
let cryptoService!: MockProxy<CryptoService>; let cryptoService!: MockProxy<CryptoService>;
let apiService!: MockProxy<ApiService>; let apiService!: MockProxy<ApiService>;
let tokenService!: MockProxy<TokenService>; let tokenService!: MockProxy<TokenService>;
@ -72,6 +74,7 @@ describe("WebAuthnLoginStrategy", () => {
tokenService.decodeToken.mockResolvedValue({}); tokenService.decodeToken.mockResolvedValue({});
webAuthnLoginStrategy = new WebAuthnLoginStrategy( webAuthnLoginStrategy = new WebAuthnLoginStrategy(
cache,
cryptoService, cryptoService,
apiService, apiService,
tokenService, tokenService,
@ -286,7 +289,7 @@ function randomBytes(length: number): Uint8Array {
// AuthenticatorAssertionResponse && PublicKeyCredential are only available in secure contexts // AuthenticatorAssertionResponse && PublicKeyCredential are only available in secure contexts
// so we need to mock them and assign them to the global object to make them available // so we need to mock them and assign them to the global object to make them available
// for the tests // for the tests
class MockAuthenticatorAssertionResponse implements AuthenticatorAssertionResponse { export class MockAuthenticatorAssertionResponse implements AuthenticatorAssertionResponse {
clientDataJSON: ArrayBuffer = randomBytes(32).buffer; clientDataJSON: ArrayBuffer = randomBytes(32).buffer;
authenticatorData: ArrayBuffer = randomBytes(196).buffer; authenticatorData: ArrayBuffer = randomBytes(196).buffer;
signature: ArrayBuffer = randomBytes(72).buffer; signature: ArrayBuffer = randomBytes(72).buffer;
@ -298,7 +301,7 @@ class MockAuthenticatorAssertionResponse implements AuthenticatorAssertionRespon
userHandleB64Str = Utils.fromBufferToUrlB64(this.userHandle); userHandleB64Str = Utils.fromBufferToUrlB64(this.userHandle);
} }
class MockPublicKeyCredential implements PublicKeyCredential { export class MockPublicKeyCredential implements PublicKeyCredential {
authenticatorAttachment = "cross-platform"; authenticatorAttachment = "cross-platform";
id = "mockCredentialId"; id = "mockCredentialId";
type = "public-key"; type = "public-key";

View File

@ -1,16 +1,86 @@
import { BehaviorSubject } from "rxjs";
import { Jsonify } from "type-fest";
import { ApiService } from "@bitwarden/common/abstractions/api.service";
import { TokenService } from "@bitwarden/common/auth/abstractions/token.service";
import { TwoFactorService } from "@bitwarden/common/auth/abstractions/two-factor.service";
import { AuthResult } from "@bitwarden/common/auth/models/domain/auth-result"; 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 { 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 { IdentityTokenResponse } from "@bitwarden/common/auth/models/response/identity-token.response";
import { AppIdService } from "@bitwarden/common/platform/abstractions/app-id.service";
import { CryptoService } from "@bitwarden/common/platform/abstractions/crypto.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 { StateService } from "@bitwarden/common/platform/abstractions/state.service";
import { SymmetricCryptoKey } from "@bitwarden/common/platform/models/domain/symmetric-crypto-key"; import { SymmetricCryptoKey } from "@bitwarden/common/platform/models/domain/symmetric-crypto-key";
import { UserKey } from "@bitwarden/common/types/key"; import { UserKey } from "@bitwarden/common/types/key";
import { WebAuthnLoginCredentials } from "../models/domain/login-credentials"; import { WebAuthnLoginCredentials } from "../models/domain/login-credentials";
import { CacheData } from "../services/login-strategies/login-strategy.state";
import { LoginStrategy } from "./login.strategy"; import { LoginStrategy, LoginStrategyData } from "./login.strategy";
export class WebAuthnLoginStrategyData implements LoginStrategyData {
tokenRequest: WebAuthnLoginTokenRequest;
captchaBypassToken?: string;
credentials: WebAuthnLoginCredentials;
static fromJSON(obj: Jsonify<WebAuthnLoginStrategyData>): WebAuthnLoginStrategyData {
return Object.assign(new WebAuthnLoginStrategyData(), obj, {
tokenRequest: WebAuthnLoginTokenRequest.fromJSON(obj.tokenRequest),
credentials: WebAuthnLoginCredentials.fromJSON(obj.credentials),
});
}
}
export class WebAuthnLoginStrategy extends LoginStrategy { export class WebAuthnLoginStrategy extends LoginStrategy {
tokenRequest: WebAuthnLoginTokenRequest; protected cache: BehaviorSubject<WebAuthnLoginStrategyData>;
private credentials: WebAuthnLoginCredentials;
constructor(
data: WebAuthnLoginStrategyData,
cryptoService: CryptoService,
apiService: ApiService,
tokenService: TokenService,
appIdService: AppIdService,
platformUtilsService: PlatformUtilsService,
messagingService: MessagingService,
logService: LogService,
stateService: StateService,
twoFactorService: TwoFactorService,
) {
super(
cryptoService,
apiService,
tokenService,
appIdService,
platformUtilsService,
messagingService,
logService,
stateService,
twoFactorService,
);
this.cache = new BehaviorSubject(data);
}
async logIn(credentials: WebAuthnLoginCredentials) {
const data = new WebAuthnLoginStrategyData();
data.credentials = credentials;
data.tokenRequest = new WebAuthnLoginTokenRequest(
credentials.token,
credentials.deviceResponse,
await this.buildDeviceRequest(),
);
this.cache.next(data);
const [authResult] = await this.startLogIn();
return authResult;
}
async logInTwoFactor(): Promise<AuthResult> {
throw new Error("2FA not supported yet for WebAuthn Login.");
}
protected override async setMasterKey() { protected override async setMasterKey() {
return Promise.resolve(); return Promise.resolve();
@ -29,15 +99,16 @@ export class WebAuthnLoginStrategy extends LoginStrategy {
if (userDecryptionOptions?.webAuthnPrfOption) { if (userDecryptionOptions?.webAuthnPrfOption) {
const webAuthnPrfOption = idTokenResponse.userDecryptionOptions?.webAuthnPrfOption; const webAuthnPrfOption = idTokenResponse.userDecryptionOptions?.webAuthnPrfOption;
const credentials = this.cache.value.credentials;
// confirm we still have the prf key // confirm we still have the prf key
if (!this.credentials.prfKey) { if (!credentials.prfKey) {
return; return;
} }
// decrypt prf encrypted private key // decrypt prf encrypted private key
const privateKey = await this.cryptoService.decryptToBytes( const privateKey = await this.cryptoService.decryptToBytes(
webAuthnPrfOption.encryptedPrivateKey, webAuthnPrfOption.encryptedPrivateKey,
this.credentials.prfKey, credentials.prfKey,
); );
// decrypt user key with private key // decrypt user key with private key
@ -58,22 +129,9 @@ export class WebAuthnLoginStrategy extends LoginStrategy {
); );
} }
async logInTwoFactor(): Promise<AuthResult> { exportCache(): CacheData {
throw new Error("2FA not supported yet for WebAuthn Login."); return {
} webAuthn: this.cache.value,
};
async logIn(credentials: WebAuthnLoginCredentials) {
// NOTE: To avoid DeadObject references on Firefox, do not set the credentials object directly
// Use deep copy in future if objects are added that were created in popup
this.credentials = { ...credentials };
this.tokenRequest = new WebAuthnLoginTokenRequest(
credentials.token,
credentials.deviceResponse,
await this.buildDeviceRequest(),
);
const [authResult] = await this.startLogIn();
return authResult;
} }
} }

View File

@ -1,3 +1,5 @@
import { Jsonify } from "type-fest";
import { AuthenticationType } from "@bitwarden/common/auth/enums/authentication-type"; import { AuthenticationType } from "@bitwarden/common/auth/enums/authentication-type";
import { TokenTwoFactorRequest } from "@bitwarden/common/auth/models/request/identity-token/token-two-factor.request"; import { TokenTwoFactorRequest } from "@bitwarden/common/auth/models/request/identity-token/token-two-factor.request";
import { WebAuthnLoginAssertionResponseRequest } from "@bitwarden/common/auth/services/webauthn-login/request/webauthn-login-assertion-response.request"; import { WebAuthnLoginAssertionResponseRequest } from "@bitwarden/common/auth/services/webauthn-login/request/webauthn-login-assertion-response.request";
@ -28,7 +30,7 @@ export class SsoLoginCredentials {
} }
export class UserApiLoginCredentials { export class UserApiLoginCredentials {
readonly type = AuthenticationType.UserApi; readonly type = AuthenticationType.UserApiKey;
constructor( constructor(
public clientId: string, public clientId: string,
@ -48,6 +50,30 @@ export class AuthRequestLoginCredentials {
public decryptedMasterKeyHash: string, public decryptedMasterKeyHash: string,
public twoFactor?: TokenTwoFactorRequest, public twoFactor?: TokenTwoFactorRequest,
) {} ) {}
static fromJSON(json: Jsonify<AuthRequestLoginCredentials>) {
return Object.assign(
new AuthRequestLoginCredentials(
json.email,
json.accessCode,
json.authRequestId,
null,
null,
json.decryptedMasterKeyHash,
json.twoFactor
? new TokenTwoFactorRequest(
json.twoFactor.provider,
json.twoFactor.token,
json.twoFactor.remember,
)
: json.twoFactor,
),
{
decryptedUserKey: SymmetricCryptoKey.fromJSON(json.decryptedUserKey) as UserKey,
decryptedMasterKey: SymmetricCryptoKey.fromJSON(json.decryptedMasterKey) as MasterKey,
},
);
}
} }
export class WebAuthnLoginCredentials { export class WebAuthnLoginCredentials {
@ -58,4 +84,15 @@ export class WebAuthnLoginCredentials {
public deviceResponse: WebAuthnLoginAssertionResponseRequest, public deviceResponse: WebAuthnLoginAssertionResponseRequest,
public prfKey?: SymmetricCryptoKey, public prfKey?: SymmetricCryptoKey,
) {} ) {}
static fromJSON(json: Jsonify<WebAuthnLoginCredentials>) {
return new WebAuthnLoginCredentials(
json.token,
Object.assign(
Object.create(WebAuthnLoginAssertionResponseRequest.prototype),
json.deviceResponse,
),
SymmetricCryptoKey.fromJSON(json.prfKey),
);
}
} }

View File

@ -0,0 +1,201 @@
import { MockProxy, mock } from "jest-mock-extended";
import { ApiService } from "@bitwarden/common/abstractions/api.service";
import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction";
import { DeviceTrustCryptoServiceAbstraction } from "@bitwarden/common/auth/abstractions/device-trust-crypto.service.abstraction";
import { KeyConnectorService } from "@bitwarden/common/auth/abstractions/key-connector.service";
import { TokenService } from "@bitwarden/common/auth/abstractions/token.service";
import { TwoFactorService } from "@bitwarden/common/auth/abstractions/two-factor.service";
import { TwoFactorProviderType } from "@bitwarden/common/auth/enums/two-factor-provider-type";
import { AuthResult } from "@bitwarden/common/auth/models/domain/auth-result";
import { TokenTwoFactorRequest } from "@bitwarden/common/auth/models/request/identity-token/token-two-factor.request";
import { IdentityTokenResponse } from "@bitwarden/common/auth/models/response/identity-token.response";
import { IdentityTwoFactorResponse } from "@bitwarden/common/auth/models/response/identity-two-factor.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 { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.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 { StateService } from "@bitwarden/common/platform/abstractions/state.service";
import { KdfType } from "@bitwarden/common/platform/enums";
import { FakeGlobalState, FakeGlobalStateProvider } from "@bitwarden/common/spec";
import { PasswordStrengthServiceAbstraction } from "@bitwarden/common/tools/password-strength";
import { AuthRequestServiceAbstraction } from "../../abstractions";
import { PasswordLoginCredentials } from "../../models";
import { LoginStrategyService } from "./login-strategy.service";
import { CACHE_EXPIRATION_KEY } from "./login-strategy.state";
describe("LoginStrategyService", () => {
let sut: LoginStrategyService;
let cryptoService: MockProxy<CryptoService>;
let apiService: MockProxy<ApiService>;
let tokenService: MockProxy<TokenService>;
let appIdService: MockProxy<AppIdService>;
let platformUtilsService: MockProxy<PlatformUtilsService>;
let messagingService: MockProxy<MessagingService>;
let logService: MockProxy<LogService>;
let keyConnectorService: MockProxy<KeyConnectorService>;
let environmentService: MockProxy<EnvironmentService>;
let stateService: MockProxy<StateService>;
let twoFactorService: MockProxy<TwoFactorService>;
let i18nService: MockProxy<I18nService>;
let encryptService: MockProxy<EncryptService>;
let passwordStrengthService: MockProxy<PasswordStrengthServiceAbstraction>;
let policyService: MockProxy<PolicyService>;
let deviceTrustCryptoService: MockProxy<DeviceTrustCryptoServiceAbstraction>;
let authRequestService: MockProxy<AuthRequestServiceAbstraction>;
let stateProvider: FakeGlobalStateProvider;
let loginStrategyCacheExpirationState: FakeGlobalState<Date | null>;
beforeEach(() => {
cryptoService = mock<CryptoService>();
apiService = mock<ApiService>();
tokenService = mock<TokenService>();
appIdService = mock<AppIdService>();
platformUtilsService = mock<PlatformUtilsService>();
messagingService = mock<MessagingService>();
logService = mock<LogService>();
keyConnectorService = mock<KeyConnectorService>();
environmentService = mock<EnvironmentService>();
stateService = mock<StateService>();
twoFactorService = mock<TwoFactorService>();
i18nService = mock<I18nService>();
encryptService = mock<EncryptService>();
passwordStrengthService = mock<PasswordStrengthServiceAbstraction>();
policyService = mock<PolicyService>();
deviceTrustCryptoService = mock<DeviceTrustCryptoServiceAbstraction>();
authRequestService = mock<AuthRequestServiceAbstraction>();
stateProvider = new FakeGlobalStateProvider();
sut = new LoginStrategyService(
cryptoService,
apiService,
tokenService,
appIdService,
platformUtilsService,
messagingService,
logService,
keyConnectorService,
environmentService,
stateService,
twoFactorService,
i18nService,
encryptService,
passwordStrengthService,
policyService,
deviceTrustCryptoService,
authRequestService,
stateProvider,
);
loginStrategyCacheExpirationState = stateProvider.getFake(CACHE_EXPIRATION_KEY);
});
it("should return an AuthResult on successful login", async () => {
const credentials = new PasswordLoginCredentials("EMAIL", "MASTER_PASSWORD");
apiService.postIdentityToken.mockResolvedValue(
new IdentityTokenResponse({
ForcePasswordReset: false,
Kdf: KdfType.Argon2id,
Key: "KEY",
PrivateKey: "PRIVATE_KEY",
ResetMasterPassword: false,
access_token: "ACCESS_TOKEN",
expires_in: 3600,
refresh_token: "REFRESH_TOKEN",
scope: "api offline_access",
token_type: "Bearer",
}),
);
tokenService.decodeToken.calledWith("ACCESS_TOKEN").mockResolvedValue({
sub: "USER_ID",
name: "NAME",
email: "EMAIL",
premium: false,
});
const result = await sut.logIn(credentials);
expect(result).toBeInstanceOf(AuthResult);
});
it("should return an AuthResult on successful 2fa login", async () => {
const credentials = new PasswordLoginCredentials("EMAIL", "MASTER_PASSWORD");
apiService.postIdentityToken.mockResolvedValueOnce(
new IdentityTwoFactorResponse({
TwoFactorProviders: ["0"],
TwoFactorProviders2: { 0: null },
error: "invalid_grant",
error_description: "Two factor required.",
email: undefined,
ssoEmail2faSessionToken: undefined,
}),
);
await sut.logIn(credentials);
const twoFactorToken = new TokenTwoFactorRequest(
TwoFactorProviderType.Authenticator,
"TWO_FACTOR_TOKEN",
true,
);
apiService.postIdentityToken.mockResolvedValue(
new IdentityTokenResponse({
ForcePasswordReset: false,
Kdf: KdfType.Argon2id,
Key: "KEY",
PrivateKey: "PRIVATE_KEY",
ResetMasterPassword: false,
access_token: "ACCESS_TOKEN",
expires_in: 3600,
refresh_token: "REFRESH_TOKEN",
scope: "api offline_access",
token_type: "Bearer",
}),
);
tokenService.decodeToken.calledWith("ACCESS_TOKEN").mockResolvedValue({
sub: "USER_ID",
name: "NAME",
email: "EMAIL",
premium: false,
});
const result = await sut.logInTwoFactor(twoFactorToken, "CAPTCHA");
expect(result).toBeInstanceOf(AuthResult);
});
it("should clear the cache if more than 2 mins have passed since expiration date", async () => {
const credentials = new PasswordLoginCredentials("EMAIL", "MASTER_PASSWORD");
apiService.postIdentityToken.mockResolvedValue(
new IdentityTwoFactorResponse({
TwoFactorProviders: ["0"],
TwoFactorProviders2: { 0: null },
error: "invalid_grant",
error_description: "Two factor required.",
email: undefined,
ssoEmail2faSessionToken: undefined,
}),
);
await sut.logIn(credentials);
loginStrategyCacheExpirationState.stateSubject.next(new Date(Date.now() - 1000 * 60 * 5));
const twoFactorToken = new TokenTwoFactorRequest(
TwoFactorProviderType.Authenticator,
"TWO_FACTOR_TOKEN",
true,
);
await expect(sut.logInTwoFactor(twoFactorToken, "CAPTCHA")).rejects.toThrow();
});
});

View File

@ -1,4 +1,12 @@
import { Observable, Subject } from "rxjs"; import {
combineLatestWith,
distinctUntilChanged,
filter,
firstValueFrom,
map,
Observable,
shareReplay,
} from "rxjs";
import { ApiService } from "@bitwarden/common/abstractions/api.service"; import { ApiService } from "@bitwarden/common/abstractions/api.service";
import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction"; import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction";
@ -10,6 +18,8 @@ import { AuthenticationType } from "@bitwarden/common/auth/enums/authentication-
import { AuthResult } from "@bitwarden/common/auth/models/domain/auth-result"; import { AuthResult } from "@bitwarden/common/auth/models/domain/auth-result";
import { KdfConfig } from "@bitwarden/common/auth/models/domain/kdf-config"; import { KdfConfig } from "@bitwarden/common/auth/models/domain/kdf-config";
import { TokenTwoFactorRequest } from "@bitwarden/common/auth/models/request/identity-token/token-two-factor.request"; import { TokenTwoFactorRequest } from "@bitwarden/common/auth/models/request/identity-token/token-two-factor.request";
import { PasswordlessAuthRequest } from "@bitwarden/common/auth/models/request/passwordless-auth.request";
import { AuthRequestResponse } from "@bitwarden/common/auth/models/response/auth-request.response";
import { PreloginRequest } from "@bitwarden/common/models/request/prelogin.request"; import { PreloginRequest } from "@bitwarden/common/models/request/prelogin.request";
import { ErrorResponse } from "@bitwarden/common/models/response/error.response"; import { ErrorResponse } from "@bitwarden/common/models/response/error.response";
import { AuthRequestPushNotification } from "@bitwarden/common/models/response/notification.response"; import { AuthRequestPushNotification } from "@bitwarden/common/models/response/notification.response";
@ -23,6 +33,8 @@ import { MessagingService } from "@bitwarden/common/platform/abstractions/messag
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
import { StateService } from "@bitwarden/common/platform/abstractions/state.service"; import { StateService } from "@bitwarden/common/platform/abstractions/state.service";
import { KdfType } from "@bitwarden/common/platform/enums"; import { KdfType } from "@bitwarden/common/platform/enums";
import { Utils } from "@bitwarden/common/platform/misc/utils";
import { GlobalState, GlobalStateProvider } from "@bitwarden/common/platform/state";
import { PasswordStrengthServiceAbstraction } from "@bitwarden/common/tools/password-strength"; import { PasswordStrengthServiceAbstraction } from "@bitwarden/common/tools/password-strength";
import { MasterKey } from "@bitwarden/common/types/key"; import { MasterKey } from "@bitwarden/common/types/key";
@ -40,54 +52,35 @@ import {
WebAuthnLoginCredentials, WebAuthnLoginCredentials,
} from "../../models"; } from "../../models";
import {
AUTH_REQUEST_PUSH_NOTIFICATION_KEY,
CURRENT_LOGIN_STRATEGY_KEY,
CacheData,
CACHE_EXPIRATION_KEY,
CACHE_KEY,
} from "./login-strategy.state";
const sessionTimeoutLength = 2 * 60 * 1000; // 2 minutes const sessionTimeoutLength = 2 * 60 * 1000; // 2 minutes
export class LoginStrategyService implements LoginStrategyServiceAbstraction { export class LoginStrategyService implements LoginStrategyServiceAbstraction {
get email(): string { private sessionTimeout: unknown;
if ( private currentAuthnTypeState: GlobalState<AuthenticationType | null>;
this.logInStrategy instanceof PasswordLoginStrategy || private loginStrategyCacheState: GlobalState<CacheData | null>;
this.logInStrategy instanceof AuthRequestLoginStrategy || private loginStrategyCacheExpirationState: GlobalState<Date | null>;
this.logInStrategy instanceof SsoLoginStrategy private authRequestPushNotificationState: GlobalState<string>;
) {
return this.logInStrategy.email;
}
return null; private loginStrategy$: Observable<
}
get masterPasswordHash(): string {
return this.logInStrategy instanceof PasswordLoginStrategy
? this.logInStrategy.masterPasswordHash
: null;
}
get accessCode(): string {
return this.logInStrategy instanceof AuthRequestLoginStrategy
? this.logInStrategy.accessCode
: null;
}
get authRequestId(): string {
return this.logInStrategy instanceof AuthRequestLoginStrategy
? this.logInStrategy.authRequestId
: null;
}
get ssoEmail2FaSessionToken(): string {
return this.logInStrategy instanceof SsoLoginStrategy
? this.logInStrategy.ssoEmail2FaSessionToken
: null;
}
private logInStrategy:
| UserApiLoginStrategy | UserApiLoginStrategy
| PasswordLoginStrategy | PasswordLoginStrategy
| SsoLoginStrategy | SsoLoginStrategy
| AuthRequestLoginStrategy | AuthRequestLoginStrategy
| WebAuthnLoginStrategy; | WebAuthnLoginStrategy
private sessionTimeout: any; | null
>;
private pushNotificationSubject = new Subject<string>(); currentAuthType$: Observable<AuthenticationType | null>;
// TODO: move to auth request service
authRequestPushNotification$: Observable<string>;
constructor( constructor(
protected cryptoService: CryptoService, protected cryptoService: CryptoService,
@ -107,7 +100,71 @@ export class LoginStrategyService implements LoginStrategyServiceAbstraction {
protected policyService: PolicyService, protected policyService: PolicyService,
protected deviceTrustCryptoService: DeviceTrustCryptoServiceAbstraction, protected deviceTrustCryptoService: DeviceTrustCryptoServiceAbstraction,
protected authRequestService: AuthRequestServiceAbstraction, protected authRequestService: AuthRequestServiceAbstraction,
) {} protected stateProvider: GlobalStateProvider,
) {
this.currentAuthnTypeState = this.stateProvider.get(CURRENT_LOGIN_STRATEGY_KEY);
this.loginStrategyCacheState = this.stateProvider.get(CACHE_KEY);
this.loginStrategyCacheExpirationState = this.stateProvider.get(CACHE_EXPIRATION_KEY);
this.authRequestPushNotificationState = this.stateProvider.get(
AUTH_REQUEST_PUSH_NOTIFICATION_KEY,
);
this.currentAuthType$ = this.currentAuthnTypeState.state$;
this.authRequestPushNotification$ = this.authRequestPushNotificationState.state$.pipe(
filter((id) => id != null),
);
this.loginStrategy$ = this.currentAuthnTypeState.state$.pipe(
distinctUntilChanged(),
combineLatestWith(this.loginStrategyCacheState.state$),
this.initializeLoginStrategy.bind(this),
shareReplay({ refCount: true, bufferSize: 1 }),
);
}
async getEmail(): Promise<string | null> {
const strategy = await firstValueFrom(this.loginStrategy$);
if ("email$" in strategy) {
return await firstValueFrom(strategy.email$);
}
return null;
}
async getMasterPasswordHash(): Promise<string | null> {
const strategy = await firstValueFrom(this.loginStrategy$);
if ("masterKeyHash$" in strategy) {
return await firstValueFrom(strategy.masterKeyHash$);
}
return null;
}
async getSsoEmail2FaSessionToken(): Promise<string | null> {
const strategy = await firstValueFrom(this.loginStrategy$);
if ("ssoEmail2FaSessionToken$" in strategy) {
return await firstValueFrom(strategy.ssoEmail2FaSessionToken$);
}
return null;
}
async getAccessCode(): Promise<string | null> {
const strategy = await firstValueFrom(this.loginStrategy$);
if ("accessCode$" in strategy) {
return await firstValueFrom(strategy.accessCode$);
}
return null;
}
async getAuthRequestId(): Promise<string | null> {
const strategy = await firstValueFrom(this.loginStrategy$);
if ("authRequestId$" in strategy) {
return await firstValueFrom(strategy.authRequestId$);
}
return null;
}
async logIn( async logIn(
credentials: credentials:
@ -117,99 +174,27 @@ export class LoginStrategyService implements LoginStrategyServiceAbstraction {
| AuthRequestLoginCredentials | AuthRequestLoginCredentials
| WebAuthnLoginCredentials, | WebAuthnLoginCredentials,
): Promise<AuthResult> { ): Promise<AuthResult> {
this.clearState(); await this.clearCache();
let strategy: await this.currentAuthnTypeState.update((_) => credentials.type);
| UserApiLoginStrategy
| PasswordLoginStrategy
| SsoLoginStrategy
| AuthRequestLoginStrategy
| WebAuthnLoginStrategy;
switch (credentials.type) { const strategy = await firstValueFrom(this.loginStrategy$);
case AuthenticationType.Password:
strategy = new PasswordLoginStrategy(
this.cryptoService,
this.apiService,
this.tokenService,
this.appIdService,
this.platformUtilsService,
this.messagingService,
this.logService,
this.stateService,
this.twoFactorService,
this.passwordStrengthService,
this.policyService,
this,
);
break;
case AuthenticationType.Sso:
strategy = new SsoLoginStrategy(
this.cryptoService,
this.apiService,
this.tokenService,
this.appIdService,
this.platformUtilsService,
this.messagingService,
this.logService,
this.stateService,
this.twoFactorService,
this.keyConnectorService,
this.deviceTrustCryptoService,
this.authRequestService,
this.i18nService,
);
break;
case AuthenticationType.UserApi:
strategy = new UserApiLoginStrategy(
this.cryptoService,
this.apiService,
this.tokenService,
this.appIdService,
this.platformUtilsService,
this.messagingService,
this.logService,
this.stateService,
this.twoFactorService,
this.environmentService,
this.keyConnectorService,
);
break;
case AuthenticationType.AuthRequest:
strategy = new AuthRequestLoginStrategy(
this.cryptoService,
this.apiService,
this.tokenService,
this.appIdService,
this.platformUtilsService,
this.messagingService,
this.logService,
this.stateService,
this.twoFactorService,
this.deviceTrustCryptoService,
);
break;
case AuthenticationType.WebAuthn:
strategy = new WebAuthnLoginStrategy(
this.cryptoService,
this.apiService,
this.tokenService,
this.appIdService,
this.platformUtilsService,
this.messagingService,
this.logService,
this.stateService,
this.twoFactorService,
);
break;
}
// Note: Do not set the credentials object directly on the strategy. They are // Note: We aren't passing the credentials directly to the strategy since they are
// created in the popup and can cause DeadObject references on Firefox. // created in the popup and can cause DeadObject references on Firefox.
const result = await strategy.logIn(credentials as any); // This is a shallow copy, but use deep copy in future if objects are added to credentials
// that were created in popup.
// If the popup uses its own instance of this service, this can be removed.
const ownedCredentials = { ...credentials };
if (result?.requiresTwoFactor) { const result = await strategy.logIn(ownedCredentials as any);
this.saveState(strategy);
if (result != null && !result.requiresTwoFactor) {
await this.clearCache();
} else {
// Cache the strategy data so we can attempt again later with 2fa. Cache supports different contexts
await this.loginStrategyCacheState.update((_) => strategy.exportCache());
await this.startSessionTimeout();
} }
return result; return result;
@ -219,43 +204,32 @@ export class LoginStrategyService implements LoginStrategyServiceAbstraction {
twoFactor: TokenTwoFactorRequest, twoFactor: TokenTwoFactorRequest,
captchaResponse: string, captchaResponse: string,
): Promise<AuthResult> { ): Promise<AuthResult> {
if (this.logInStrategy == null) { if (!(await this.isSessionValid())) {
throw new Error(this.i18nService.t("sessionTimeout")); throw new Error(this.i18nService.t("sessionTimeout"));
} }
try { const strategy = await firstValueFrom(this.loginStrategy$);
const result = await this.logInStrategy.logInTwoFactor(twoFactor, captchaResponse); if (strategy == null) {
throw new Error("No login strategy found.");
}
// Only clear state if 2FA token has been accepted, otherwise we need to be able to try again try {
if (!result.requiresTwoFactor && !result.requiresCaptcha) { const result = await strategy.logInTwoFactor(twoFactor, captchaResponse);
this.clearState();
// Only clear cache if 2FA token has been accepted, otherwise we need to be able to try again
if (result != null && !result.requiresTwoFactor && !result.requiresCaptcha) {
await this.clearCache();
} }
return result; return result;
} catch (e) { } catch (e) {
// API exceptions are okay, but if there are any unhandled client-side errors then clear state to be safe // API exceptions are okay, but if there are any unhandled client-side errors then clear cache to be safe
if (!(e instanceof ErrorResponse)) { if (!(e instanceof ErrorResponse)) {
this.clearState(); await this.clearCache();
} }
throw e; throw e;
} }
} }
authingWithUserApiKey(): boolean {
return this.logInStrategy instanceof UserApiLoginStrategy;
}
authingWithSso(): boolean {
return this.logInStrategy instanceof SsoLoginStrategy;
}
authingWithPassword(): boolean {
return this.logInStrategy instanceof PasswordLoginStrategy;
}
authingWithPasswordless(): boolean {
return this.logInStrategy instanceof AuthRequestLoginStrategy;
}
async makePreloginKey(masterPassword: string, email: string): Promise<MasterKey> { async makePreloginKey(masterPassword: string, email: string): Promise<MasterKey> {
email = email.trim().toLowerCase(); email = email.trim().toLowerCase();
let kdf: KdfType = null; let kdf: KdfType = null;
@ -278,39 +252,171 @@ export class LoginStrategyService implements LoginStrategyServiceAbstraction {
return await this.cryptoService.makeMasterKey(masterPassword, email, kdf, kdfConfig); return await this.cryptoService.makeMasterKey(masterPassword, email, kdf, kdfConfig);
} }
async authResponsePushNotification(notification: AuthRequestPushNotification): Promise<any> { // TODO move to auth request service
this.pushNotificationSubject.next(notification.id); async sendAuthRequestPushNotification(notification: AuthRequestPushNotification): Promise<void> {
} if (notification.id != null) {
await this.authRequestPushNotificationState.update((_) => notification.id);
getPushNotificationObs$(): Observable<any> {
return this.pushNotificationSubject.asObservable();
}
private saveState(
strategy:
| UserApiLoginStrategy
| PasswordLoginStrategy
| SsoLoginStrategy
| AuthRequestLoginStrategy
| WebAuthnLoginStrategy,
) {
this.logInStrategy = strategy;
this.startSessionTimeout();
}
private clearState() {
this.logInStrategy = null;
this.clearSessionTimeout();
}
private startSessionTimeout() {
this.clearSessionTimeout();
this.sessionTimeout = setTimeout(() => this.clearState(), sessionTimeoutLength);
}
private clearSessionTimeout() {
if (this.sessionTimeout != null) {
clearTimeout(this.sessionTimeout);
} }
} }
// TODO: move to auth request service
async passwordlessLogin(
id: string,
key: string,
requestApproved: boolean,
): Promise<AuthRequestResponse> {
const pubKey = Utils.fromB64ToArray(key);
const masterKey = await this.cryptoService.getMasterKey();
let keyToEncrypt;
let encryptedMasterKeyHash = null;
if (masterKey) {
keyToEncrypt = masterKey.encKey;
// Only encrypt the master password hash if masterKey exists as
// we won't have a masterKeyHash without a masterKey
const masterKeyHash = await this.stateService.getKeyHash();
if (masterKeyHash != null) {
encryptedMasterKeyHash = await this.cryptoService.rsaEncrypt(
Utils.fromUtf8ToArray(masterKeyHash),
pubKey,
);
}
} else {
const userKey = await this.cryptoService.getUserKey();
keyToEncrypt = userKey.key;
}
const encryptedKey = await this.cryptoService.rsaEncrypt(keyToEncrypt, pubKey);
const request = new PasswordlessAuthRequest(
encryptedKey.encryptedString,
encryptedMasterKeyHash?.encryptedString,
await this.appIdService.getAppId(),
requestApproved,
);
return await this.apiService.putAuthRequest(id, request);
}
private async clearCache(): Promise<void> {
await this.currentAuthnTypeState.update((_) => null);
await this.loginStrategyCacheState.update((_) => null);
await this.clearSessionTimeout();
}
private async startSessionTimeout(): Promise<void> {
await this.clearSessionTimeout();
await this.loginStrategyCacheExpirationState.update(
(_) => new Date(Date.now() + sessionTimeoutLength),
);
this.sessionTimeout = setTimeout(() => this.clearCache(), sessionTimeoutLength);
}
private async clearSessionTimeout(): Promise<void> {
await this.loginStrategyCacheExpirationState.update((_) => null);
this.sessionTimeout = null;
}
private async isSessionValid(): Promise<boolean> {
const cache = await firstValueFrom(this.loginStrategyCacheState.state$);
if (cache == null) {
return false;
}
const expiration = await firstValueFrom(this.loginStrategyCacheExpirationState.state$);
if (expiration != null && expiration < new Date()) {
await this.clearCache();
return false;
}
return true;
}
private initializeLoginStrategy(
source: Observable<[AuthenticationType | null, CacheData | null]>,
) {
return source.pipe(
map(([strategy, data]) => {
if (strategy == null) {
return null;
}
switch (strategy) {
case AuthenticationType.Password:
return new PasswordLoginStrategy(
data?.password,
this.cryptoService,
this.apiService,
this.tokenService,
this.appIdService,
this.platformUtilsService,
this.messagingService,
this.logService,
this.stateService,
this.twoFactorService,
this.passwordStrengthService,
this.policyService,
this,
);
case AuthenticationType.Sso:
return new SsoLoginStrategy(
data?.sso,
this.cryptoService,
this.apiService,
this.tokenService,
this.appIdService,
this.platformUtilsService,
this.messagingService,
this.logService,
this.stateService,
this.twoFactorService,
this.keyConnectorService,
this.deviceTrustCryptoService,
this.authRequestService,
this.i18nService,
);
case AuthenticationType.UserApiKey:
return new UserApiLoginStrategy(
data?.userApiKey,
this.cryptoService,
this.apiService,
this.tokenService,
this.appIdService,
this.platformUtilsService,
this.messagingService,
this.logService,
this.stateService,
this.twoFactorService,
this.environmentService,
this.keyConnectorService,
);
case AuthenticationType.AuthRequest:
return new AuthRequestLoginStrategy(
data?.authRequest,
this.cryptoService,
this.apiService,
this.tokenService,
this.appIdService,
this.platformUtilsService,
this.messagingService,
this.logService,
this.stateService,
this.twoFactorService,
this.deviceTrustCryptoService,
);
case AuthenticationType.WebAuthn:
return new WebAuthnLoginStrategy(
data?.webAuthn,
this.cryptoService,
this.apiService,
this.tokenService,
this.appIdService,
this.platformUtilsService,
this.messagingService,
this.logService,
this.stateService,
this.twoFactorService,
);
}
}),
);
}
} }

View File

@ -0,0 +1,155 @@
import { TwoFactorProviderType } from "@bitwarden/common/auth/enums/two-factor-provider-type";
import { DeviceRequest } from "@bitwarden/common/auth/models/request/identity-token/device.request";
import { PasswordTokenRequest } from "@bitwarden/common/auth/models/request/identity-token/password-token.request";
import { SsoTokenRequest } from "@bitwarden/common/auth/models/request/identity-token/sso-token.request";
import { TokenTwoFactorRequest } from "@bitwarden/common/auth/models/request/identity-token/token-two-factor.request";
import { UserApiTokenRequest } from "@bitwarden/common/auth/models/request/identity-token/user-api-token.request";
import { WebAuthnLoginTokenRequest } from "@bitwarden/common/auth/models/request/identity-token/webauthn-login-token.request";
import { WebAuthnLoginAssertionResponseRequest } from "@bitwarden/common/auth/services/webauthn-login/request/webauthn-login-assertion-response.request";
import { DeviceType } from "@bitwarden/common/enums";
import { SymmetricCryptoKey } from "@bitwarden/common/platform/models/domain/symmetric-crypto-key";
import { MasterKey, PrfKey, UserKey } from "@bitwarden/common/types/key";
import { AuthRequestLoginStrategyData } from "../../login-strategies/auth-request-login.strategy";
import { PasswordLoginStrategyData } from "../../login-strategies/password-login.strategy";
import { SsoLoginStrategyData } from "../../login-strategies/sso-login.strategy";
import { UserApiLoginStrategyData } from "../../login-strategies/user-api-login.strategy";
import { WebAuthnLoginStrategyData } from "../../login-strategies/webauthn-login.strategy";
import {
MockAuthenticatorAssertionResponse,
MockPublicKeyCredential,
} from "../../login-strategies/webauthn-login.strategy.spec";
import { AuthRequestLoginCredentials, WebAuthnLoginCredentials } from "../../models";
import { CACHE_KEY } from "./login-strategy.state";
describe("LOGIN_STRATEGY_CACHE_KEY", () => {
const sut = CACHE_KEY;
let deviceRequest: DeviceRequest;
let twoFactorRequest: TokenTwoFactorRequest;
beforeEach(() => {
deviceRequest = Object.assign(Object.create(DeviceRequest.prototype), {
type: DeviceType.ChromeBrowser,
name: "DEVICE_NAME",
identifier: "DEVICE_IDENTIFIER",
pushToken: "PUSH_TOKEN",
});
twoFactorRequest = new TokenTwoFactorRequest(TwoFactorProviderType.Email, "TOKEN", false);
});
it("should correctly deserialize PasswordLoginStrategyData", () => {
const actual = {
password: new PasswordLoginStrategyData(),
};
actual.password.tokenRequest = new PasswordTokenRequest(
"EMAIL",
"LOCAL_PASSWORD_HASH",
"CAPTCHA_TOKEN",
twoFactorRequest,
deviceRequest,
);
actual.password.masterKey = new SymmetricCryptoKey(new Uint8Array(64)) as MasterKey;
actual.password.localMasterKeyHash = "LOCAL_MASTER_KEY_HASH";
const result = sut.deserializer(JSON.parse(JSON.stringify(actual)));
expect(result.password).toBeInstanceOf(PasswordLoginStrategyData);
verifyPropertyPrototypes(result, actual);
});
it("should correctly deserialize SsoLoginStrategyData", () => {
const actual = { sso: new SsoLoginStrategyData() };
actual.sso.tokenRequest = new SsoTokenRequest(
"CODE",
"CODE_VERIFIER",
"REDIRECT_URI",
twoFactorRequest,
deviceRequest,
);
const result = sut.deserializer(JSON.parse(JSON.stringify(actual)));
expect(result.sso).toBeInstanceOf(SsoLoginStrategyData);
verifyPropertyPrototypes(result, actual);
});
it("should correctly deserialize UserApiLoginStrategyData", () => {
const actual = { userApiKey: new UserApiLoginStrategyData() };
actual.userApiKey.tokenRequest = new UserApiTokenRequest("CLIENT_ID", "CLIENT_SECRET", null);
const result = sut.deserializer(JSON.parse(JSON.stringify(actual)));
expect(result.userApiKey).toBeInstanceOf(UserApiLoginStrategyData);
verifyPropertyPrototypes(result, actual);
});
it("should correctly deserialize AuthRequestLoginStrategyData", () => {
const actual = { authRequest: new AuthRequestLoginStrategyData() };
actual.authRequest.tokenRequest = new PasswordTokenRequest("EMAIL", "ACCESS_CODE", null, null);
actual.authRequest.authRequestCredentials = new AuthRequestLoginCredentials(
"EMAIL",
"ACCESS_CODE",
"AUTH_REQUEST_ID",
new SymmetricCryptoKey(new Uint8Array(64)) as UserKey,
new SymmetricCryptoKey(new Uint8Array(64)) as MasterKey,
"MASTER_KEY_HASH",
);
const result = sut.deserializer(JSON.parse(JSON.stringify(actual)));
expect(result.authRequest).toBeInstanceOf(AuthRequestLoginStrategyData);
verifyPropertyPrototypes(result, actual);
});
it("should correctly deserialize WebAuthnLoginStrategyData", () => {
global.AuthenticatorAssertionResponse = MockAuthenticatorAssertionResponse;
const actual = { webAuthn: new WebAuthnLoginStrategyData() };
const publicKeyCredential = new MockPublicKeyCredential();
const deviceResponse = new WebAuthnLoginAssertionResponseRequest(publicKeyCredential);
const prfKey = new SymmetricCryptoKey(new Uint8Array(64)) as PrfKey;
actual.webAuthn.credentials = new WebAuthnLoginCredentials("TOKEN", deviceResponse, prfKey);
actual.webAuthn.tokenRequest = new WebAuthnLoginTokenRequest(
"TOKEN",
deviceResponse,
deviceRequest,
);
actual.webAuthn.captchaBypassToken = "CAPTCHA_BYPASS_TOKEN";
actual.webAuthn.tokenRequest.setTwoFactor(
new TokenTwoFactorRequest(TwoFactorProviderType.Email, "TOKEN", false),
);
const result = sut.deserializer(JSON.parse(JSON.stringify(actual)));
expect(result.webAuthn).toBeInstanceOf(WebAuthnLoginStrategyData);
verifyPropertyPrototypes(result, actual);
});
});
/**
* Recursively verifies the prototypes of all objects in the deserialized object.
* It is important that the concrete object has the correct prototypes for
* comparison.
* @param deserialized the deserialized object
* @param concrete the object stored in state
*/
function verifyPropertyPrototypes(deserialized: object, concrete: object) {
for (const key of Object.keys(deserialized)) {
const deserializedProperty = (deserialized as any)[key];
if (deserializedProperty === undefined) {
continue;
}
const realProperty = (concrete as any)[key];
if (realProperty === undefined) {
throw new Error(`Expected ${key} to be defined in ${concrete.constructor.name}`);
}
// we only care about checking prototypes of objects
if (typeof realProperty === "object" && realProperty !== null) {
const realProto = Object.getPrototypeOf(realProperty);
expect(deserializedProperty).toBeInstanceOf(realProto.constructor);
verifyPropertyPrototypes(deserializedProperty, realProperty);
}
}
}

View File

@ -0,0 +1,80 @@
import { AuthenticationType } from "@bitwarden/common/auth/enums/authentication-type";
import { KeyDefinition, LOGIN_STRATEGY_MEMORY } from "@bitwarden/common/platform/state";
import { AuthRequestLoginStrategyData } from "../../login-strategies/auth-request-login.strategy";
import { PasswordLoginStrategyData } from "../../login-strategies/password-login.strategy";
import { SsoLoginStrategyData } from "../../login-strategies/sso-login.strategy";
import { UserApiLoginStrategyData } from "../../login-strategies/user-api-login.strategy";
import { WebAuthnLoginStrategyData } from "../../login-strategies/webauthn-login.strategy";
/**
* The current login strategy in use.
*/
export const CURRENT_LOGIN_STRATEGY_KEY = new KeyDefinition<AuthenticationType | null>(
LOGIN_STRATEGY_MEMORY,
"currentLoginStrategy",
{
deserializer: (data) => data,
},
);
/**
* The expiration date for the login strategy cache.
* Used as a backup to the timer set on the service.
*/
export const CACHE_EXPIRATION_KEY = new KeyDefinition<Date | null>(
LOGIN_STRATEGY_MEMORY,
"loginStrategyCacheExpiration",
{
deserializer: (data) => (data ? null : new Date(data)),
},
);
/**
* Auth Request notification for all instances of the login strategy service.
* Note: this isn't an ideal approach, but allows both a background and
* foreground instance to send out the notification.
* TODO: Move to Auth Request service.
*/
export const AUTH_REQUEST_PUSH_NOTIFICATION_KEY = new KeyDefinition<string>(
LOGIN_STRATEGY_MEMORY,
"authRequestPushNotification",
{
deserializer: (data) => data,
},
);
export type CacheData = {
password?: PasswordLoginStrategyData;
sso?: SsoLoginStrategyData;
userApiKey?: UserApiLoginStrategyData;
authRequest?: AuthRequestLoginStrategyData;
webAuthn?: WebAuthnLoginStrategyData;
};
/**
* A cache for login strategies to use for data persistence through
* the login process.
*/
export const CACHE_KEY = new KeyDefinition<CacheData | null>(
LOGIN_STRATEGY_MEMORY,
"loginStrategyCache",
{
deserializer: (data) => {
if (data == null) {
return null;
}
return {
password: data.password ? PasswordLoginStrategyData.fromJSON(data.password) : undefined,
sso: data.sso ? SsoLoginStrategyData.fromJSON(data.sso) : undefined,
userApiKey: data.userApiKey
? UserApiLoginStrategyData.fromJSON(data.userApiKey)
: undefined,
authRequest: data.authRequest
? AuthRequestLoginStrategyData.fromJSON(data.authRequest)
: undefined,
webAuthn: data.webAuthn ? WebAuthnLoginStrategyData.fromJSON(data.webAuthn) : undefined,
};
},
},
);

View File

@ -1,14 +1,8 @@
import { Observable } from "rxjs"; import { Observable } from "rxjs";
import { AccountSettingsSettings } from "../platform/models/domain/account";
export abstract class SettingsService { export abstract class SettingsService {
settings$: Observable<AccountSettingsSettings>;
disableFavicon$: Observable<boolean>; disableFavicon$: Observable<boolean>;
setEquivalentDomains: (equivalentDomains: string[][]) => Promise<any>;
getEquivalentDomains: (url: string) => Set<string>;
setDisableFavicon: (value: boolean) => Promise<any>; setDisableFavicon: (value: boolean) => Promise<any>;
getDisableFavicon: () => boolean; getDisableFavicon: () => boolean;
clear: (userId?: string) => Promise<void>;
} }

View File

@ -1,7 +1,7 @@
export enum AuthenticationType { export enum AuthenticationType {
Password = 0, Password = 0,
Sso = 1, Sso = 1,
UserApi = 2, UserApiKey = 2,
AuthRequest = 3, AuthRequest = 3,
WebAuthn = 4, WebAuthn = 4,
} }

View File

@ -1,3 +1,5 @@
import { Jsonify } from "type-fest";
import { DeviceType } from "../../../../enums"; import { DeviceType } from "../../../../enums";
import { PlatformUtilsService } from "../../../../platform/abstractions/platform-utils.service"; import { PlatformUtilsService } from "../../../../platform/abstractions/platform-utils.service";
@ -13,4 +15,8 @@ export class DeviceRequest {
this.identifier = appId; this.identifier = appId;
this.pushToken = null; this.pushToken = null;
} }
static fromJSON(json: Jsonify<DeviceRequest>) {
return Object.assign(Object.create(DeviceRequest.prototype), json);
}
} }

View File

@ -34,4 +34,13 @@ export class PasswordTokenRequest extends TokenRequest implements CaptchaProtect
alterIdentityTokenHeaders(headers: Headers) { alterIdentityTokenHeaders(headers: Headers) {
headers.set("Auth-Email", Utils.fromUtf8ToUrlB64(this.email)); headers.set("Auth-Email", Utils.fromUtf8ToUrlB64(this.email));
} }
static fromJSON(json: any) {
return Object.assign(Object.create(PasswordTokenRequest.prototype), json, {
device: json.device ? DeviceRequest.fromJSON(json.device) : undefined,
twoFactor: json.twoFactor
? Object.assign(new TokenTwoFactorRequest(), json.twoFactor)
: undefined,
});
}
} }

View File

@ -23,4 +23,13 @@ export class SsoTokenRequest extends TokenRequest {
return obj; return obj;
} }
static fromJSON(json: any) {
return Object.assign(Object.create(SsoTokenRequest.prototype), json, {
device: json.device ? DeviceRequest.fromJSON(json.device) : undefined,
twoFactor: json.twoFactor
? Object.assign(new TokenTwoFactorRequest(), json.twoFactor)
: undefined,
});
}
} }

View File

@ -21,4 +21,13 @@ export class UserApiTokenRequest extends TokenRequest {
return obj; return obj;
} }
static fromJSON(json: any) {
return Object.assign(Object.create(UserApiTokenRequest.prototype), json, {
device: json.device ? DeviceRequest.fromJSON(json.device) : undefined,
twoFactor: json.twoFactor
? Object.assign(new TokenTwoFactorRequest(), json.twoFactor)
: undefined,
});
}
} }

View File

@ -1,6 +1,7 @@
import { WebAuthnLoginAssertionResponseRequest } from "../../../services/webauthn-login/request/webauthn-login-assertion-response.request"; import { WebAuthnLoginAssertionResponseRequest } from "../../../services/webauthn-login/request/webauthn-login-assertion-response.request";
import { DeviceRequest } from "./device.request"; import { DeviceRequest } from "./device.request";
import { TokenTwoFactorRequest } from "./token-two-factor.request";
import { TokenRequest } from "./token.request"; import { TokenRequest } from "./token.request";
export class WebAuthnLoginTokenRequest extends TokenRequest { export class WebAuthnLoginTokenRequest extends TokenRequest {
@ -22,4 +23,14 @@ export class WebAuthnLoginTokenRequest extends TokenRequest {
return obj; return obj;
} }
static fromJSON(json: any) {
return Object.assign(Object.create(WebAuthnLoginTokenRequest.prototype), json, {
deviceResponse: WebAuthnLoginAssertionResponseRequest.fromJSON(json.deviceResponse),
device: json.device ? DeviceRequest.fromJSON(json.device) : undefined,
twoFactor: json.twoFactor
? Object.assign(new TokenTwoFactorRequest(), json.twoFactor)
: undefined,
});
}
} }

View File

@ -54,7 +54,7 @@ export class AnonymousHubService implements AnonymousHubServiceAbstraction {
} }
private async ProcessNotification(notification: NotificationResponse) { private async ProcessNotification(notification: NotificationResponse) {
await this.loginStrategyService.authResponsePushNotification( await this.loginStrategyService.sendAuthRequestPushNotification(
notification.payload as AuthRequestPushNotification, notification.payload as AuthRequestPushNotification,
); );
} }

View File

@ -1,17 +1,22 @@
import { mock, MockProxy } from "jest-mock-extended"; import { mock, MockProxy } from "jest-mock-extended";
import { BehaviorSubject } from "rxjs";
import { UserId } from "../../../../common/src/types/guid";
import { OrganizationApiServiceAbstraction } from "../../admin-console/abstractions/organization/organization-api.service.abstraction"; import { OrganizationApiServiceAbstraction } from "../../admin-console/abstractions/organization/organization-api.service.abstraction";
import { OrganizationUserService } from "../../admin-console/abstractions/organization-user/organization-user.service"; import { OrganizationUserService } from "../../admin-console/abstractions/organization-user/organization-user.service";
import { OrganizationAutoEnrollStatusResponse } from "../../admin-console/models/response/organization-auto-enroll-status.response"; import { OrganizationAutoEnrollStatusResponse } from "../../admin-console/models/response/organization-auto-enroll-status.response";
import { CryptoService } from "../../platform/abstractions/crypto.service"; import { CryptoService } from "../../platform/abstractions/crypto.service";
import { I18nService } from "../../platform/abstractions/i18n.service"; import { I18nService } from "../../platform/abstractions/i18n.service";
import { StateService } from "../../platform/abstractions/state.service"; import { AccountInfo, AccountService } from "../abstractions/account.service";
import { AuthenticationStatus } from "../enums/authentication-status";
import { PasswordResetEnrollmentServiceImplementation } from "./password-reset-enrollment.service.implementation"; import { PasswordResetEnrollmentServiceImplementation } from "./password-reset-enrollment.service.implementation";
describe("PasswordResetEnrollmentServiceImplementation", () => { describe("PasswordResetEnrollmentServiceImplementation", () => {
const activeAccountSubject = new BehaviorSubject<{ id: UserId } & AccountInfo>(null);
let organizationApiService: MockProxy<OrganizationApiServiceAbstraction>; let organizationApiService: MockProxy<OrganizationApiServiceAbstraction>;
let stateService: MockProxy<StateService>; let accountService: MockProxy<AccountService>;
let cryptoService: MockProxy<CryptoService>; let cryptoService: MockProxy<CryptoService>;
let organizationUserService: MockProxy<OrganizationUserService>; let organizationUserService: MockProxy<OrganizationUserService>;
let i18nService: MockProxy<I18nService>; let i18nService: MockProxy<I18nService>;
@ -19,13 +24,14 @@ describe("PasswordResetEnrollmentServiceImplementation", () => {
beforeEach(() => { beforeEach(() => {
organizationApiService = mock<OrganizationApiServiceAbstraction>(); organizationApiService = mock<OrganizationApiServiceAbstraction>();
stateService = mock<StateService>(); accountService = mock<AccountService>();
accountService.activeAccount$ = activeAccountSubject;
cryptoService = mock<CryptoService>(); cryptoService = mock<CryptoService>();
organizationUserService = mock<OrganizationUserService>(); organizationUserService = mock<OrganizationUserService>();
i18nService = mock<I18nService>(); i18nService = mock<I18nService>();
service = new PasswordResetEnrollmentServiceImplementation( service = new PasswordResetEnrollmentServiceImplementation(
organizationApiService, organizationApiService,
stateService, accountService,
cryptoService, cryptoService,
organizationUserService, organizationUserService,
i18nService, i18nService,
@ -81,7 +87,14 @@ describe("PasswordResetEnrollmentServiceImplementation", () => {
}; };
const encryptedKey = { encryptedString: "encryptedString" }; const encryptedKey = { encryptedString: "encryptedString" };
organizationApiService.getKeys.mockResolvedValue(orgKeyResponse as any); organizationApiService.getKeys.mockResolvedValue(orgKeyResponse as any);
stateService.getUserId.mockResolvedValue("userId");
const user1AccountInfo: AccountInfo = {
name: "Test User 1",
email: "test1@email.com",
status: AuthenticationStatus.Unlocked,
};
activeAccountSubject.next(Object.assign(user1AccountInfo, { id: "userId" as UserId }));
cryptoService.getUserKey.mockResolvedValue({ key: "key" } as any); cryptoService.getUserKey.mockResolvedValue({ key: "key" } as any);
cryptoService.rsaEncrypt.mockResolvedValue(encryptedKey as any); cryptoService.rsaEncrypt.mockResolvedValue(encryptedKey as any);

View File

@ -1,11 +1,13 @@
import { firstValueFrom, map } from "rxjs";
import { OrganizationApiServiceAbstraction } from "../../admin-console/abstractions/organization/organization-api.service.abstraction"; import { OrganizationApiServiceAbstraction } from "../../admin-console/abstractions/organization/organization-api.service.abstraction";
import { OrganizationUserService } from "../../admin-console/abstractions/organization-user/organization-user.service"; import { OrganizationUserService } from "../../admin-console/abstractions/organization-user/organization-user.service";
import { OrganizationUserResetPasswordEnrollmentRequest } from "../../admin-console/abstractions/organization-user/requests"; import { OrganizationUserResetPasswordEnrollmentRequest } from "../../admin-console/abstractions/organization-user/requests";
import { CryptoService } from "../../platform/abstractions/crypto.service"; import { CryptoService } from "../../platform/abstractions/crypto.service";
import { I18nService } from "../../platform/abstractions/i18n.service"; import { I18nService } from "../../platform/abstractions/i18n.service";
import { StateService } from "../../platform/abstractions/state.service";
import { Utils } from "../../platform/misc/utils"; import { Utils } from "../../platform/misc/utils";
import { UserKey } from "../../types/key"; import { UserKey } from "../../types/key";
import { AccountService } from "../abstractions/account.service";
import { PasswordResetEnrollmentServiceAbstraction } from "../abstractions/password-reset-enrollment.service.abstraction"; import { PasswordResetEnrollmentServiceAbstraction } from "../abstractions/password-reset-enrollment.service.abstraction";
export class PasswordResetEnrollmentServiceImplementation export class PasswordResetEnrollmentServiceImplementation
@ -13,7 +15,7 @@ export class PasswordResetEnrollmentServiceImplementation
{ {
constructor( constructor(
protected organizationApiService: OrganizationApiServiceAbstraction, protected organizationApiService: OrganizationApiServiceAbstraction,
protected stateService: StateService, protected accountService: AccountService,
protected cryptoService: CryptoService, protected cryptoService: CryptoService,
protected organizationUserService: OrganizationUserService, protected organizationUserService: OrganizationUserService,
protected i18nService: I18nService, protected i18nService: I18nService,
@ -38,7 +40,8 @@ export class PasswordResetEnrollmentServiceImplementation
const orgPublicKey = Utils.fromB64ToArray(orgKeyResponse.publicKey); const orgPublicKey = Utils.fromB64ToArray(orgKeyResponse.publicKey);
userId = userId ?? (await this.stateService.getUserId()); userId =
userId ?? (await firstValueFrom(this.accountService.activeAccount$.pipe(map((a) => a?.id))));
userKey = userKey ?? (await this.cryptoService.getUserKey(userId)); userKey = userKey ?? (await this.cryptoService.getUserKey(userId));
// RSA Encrypt user's userKey.key with organization public key // RSA Encrypt user's userKey.key with organization public key
const encryptedKey = await this.cryptoService.rsaEncrypt(userKey.key, orgPublicKey); const encryptedKey = await this.cryptoService.rsaEncrypt(userKey.key, orgPublicKey);

View File

@ -1,3 +1,5 @@
import { Jsonify } from "type-fest";
import { Utils } from "../../../../platform/misc/utils"; import { Utils } from "../../../../platform/misc/utils";
import { WebAuthnLoginResponseRequest } from "./webauthn-login-response.request"; import { WebAuthnLoginResponseRequest } from "./webauthn-login-response.request";
@ -27,4 +29,8 @@ export class WebAuthnLoginAssertionResponseRequest extends WebAuthnLoginResponse
userHandle: Utils.fromBufferToUrlB64(credential.response.userHandle), userHandle: Utils.fromBufferToUrlB64(credential.response.userHandle),
}; };
} }
static fromJSON(json: Jsonify<WebAuthnLoginAssertionResponseRequest>) {
return Object.assign(Object.create(WebAuthnLoginAssertionResponseRequest.prototype), json);
}
} }

View File

@ -0,0 +1,53 @@
import { firstValueFrom, of } from "rxjs";
import { FakeStateProvider, FakeAccountService, mockAccountServiceWith } from "../../../spec";
import { Utils } from "../../platform/misc/utils";
import { UserId } from "../../types/guid";
import { DefaultDomainSettingsService, DomainSettingsService } from "./domain-settings.service";
describe("DefaultDomainSettingsService", () => {
let domainSettingsService: DomainSettingsService;
const mockUserId = Utils.newGuid() as UserId;
const accountService: FakeAccountService = mockAccountServiceWith(mockUserId);
const fakeStateProvider: FakeStateProvider = new FakeStateProvider(accountService);
const mockEquivalentDomains = [
["example.com", "exampleapp.com", "example.co.uk", "ejemplo.es"],
["bitwarden.com", "bitwarden.co.uk", "sm-bitwarden.com"],
["example.co.uk", "exampleapp.co.uk"],
];
beforeEach(() => {
domainSettingsService = new DefaultDomainSettingsService(fakeStateProvider);
jest.spyOn(domainSettingsService, "getUrlEquivalentDomains");
domainSettingsService.equivalentDomains$ = of(mockEquivalentDomains);
});
describe("getUrlEquivalentDomains", () => {
it("returns all equivalent domains for a URL", async () => {
const expected = new Set([
"example.com",
"exampleapp.com",
"example.co.uk",
"ejemplo.es",
"exampleapp.co.uk",
]);
const actual = await firstValueFrom(
domainSettingsService.getUrlEquivalentDomains("example.co.uk"),
);
expect(domainSettingsService.getUrlEquivalentDomains).toHaveBeenCalledWith("example.co.uk");
expect(actual).toEqual(expected);
});
it("returns an empty set if there are no equivalent domains", async () => {
const actual = await firstValueFrom(domainSettingsService.getUrlEquivalentDomains("asdf"));
expect(domainSettingsService.getUrlEquivalentDomains).toHaveBeenCalledWith("asdf");
expect(actual).toEqual(new Set());
});
});
});

View File

@ -0,0 +1,97 @@
import { map, Observable } from "rxjs";
import {
NeverDomains,
EquivalentDomains,
UriMatchStrategySetting,
UriMatchStrategy,
} from "../../models/domain/domain-service";
import { Utils } from "../../platform/misc/utils";
import {
DOMAIN_SETTINGS_DISK,
ActiveUserState,
GlobalState,
KeyDefinition,
StateProvider,
UserKeyDefinition,
} from "../../platform/state";
const NEVER_DOMAINS = new KeyDefinition(DOMAIN_SETTINGS_DISK, "neverDomains", {
deserializer: (value: NeverDomains) => value ?? null,
});
const EQUIVALENT_DOMAINS = new UserKeyDefinition(DOMAIN_SETTINGS_DISK, "equivalentDomains", {
deserializer: (value: EquivalentDomains) => value ?? null,
clearOn: ["logout"],
});
const DEFAULT_URI_MATCH_STRATEGY = new KeyDefinition(
DOMAIN_SETTINGS_DISK,
"defaultUriMatchStrategy",
{
deserializer: (value: UriMatchStrategySetting) => value ?? UriMatchStrategy.Domain,
},
);
export abstract class DomainSettingsService {
neverDomains$: Observable<NeverDomains>;
setNeverDomains: (newValue: NeverDomains) => Promise<void>;
equivalentDomains$: Observable<EquivalentDomains>;
setEquivalentDomains: (newValue: EquivalentDomains) => Promise<void>;
defaultUriMatchStrategy$: Observable<UriMatchStrategySetting>;
setDefaultUriMatchStrategy: (newValue: UriMatchStrategySetting) => Promise<void>;
getUrlEquivalentDomains: (url: string) => Observable<Set<string>>;
}
export class DefaultDomainSettingsService implements DomainSettingsService {
private neverDomainsState: GlobalState<NeverDomains>;
readonly neverDomains$: Observable<NeverDomains>;
private equivalentDomainsState: ActiveUserState<EquivalentDomains>;
readonly equivalentDomains$: Observable<EquivalentDomains>;
private defaultUriMatchStrategyState: ActiveUserState<UriMatchStrategySetting>;
readonly defaultUriMatchStrategy$: Observable<UriMatchStrategySetting>;
constructor(private stateProvider: StateProvider) {
this.neverDomainsState = this.stateProvider.getGlobal(NEVER_DOMAINS);
this.neverDomains$ = this.neverDomainsState.state$.pipe(map((x) => x ?? null));
this.equivalentDomainsState = this.stateProvider.getActive(EQUIVALENT_DOMAINS);
this.equivalentDomains$ = this.equivalentDomainsState.state$.pipe(map((x) => x ?? null));
this.defaultUriMatchStrategyState = this.stateProvider.getActive(DEFAULT_URI_MATCH_STRATEGY);
this.defaultUriMatchStrategy$ = this.defaultUriMatchStrategyState.state$.pipe(
map((x) => x ?? UriMatchStrategy.Domain),
);
}
async setNeverDomains(newValue: NeverDomains): Promise<void> {
await this.neverDomainsState.update(() => newValue);
}
async setEquivalentDomains(newValue: EquivalentDomains): Promise<void> {
await this.equivalentDomainsState.update(() => newValue);
}
async setDefaultUriMatchStrategy(newValue: UriMatchStrategySetting): Promise<void> {
await this.defaultUriMatchStrategyState.update(() => newValue);
}
getUrlEquivalentDomains(url: string): Observable<Set<string>> {
const domains$ = this.equivalentDomains$.pipe(
map((equivalentDomains) => {
const domain = Utils.getDomain(url);
if (domain == null || equivalentDomains == null) {
return new Set() as Set<string>;
}
const equivalents = equivalentDomains.filter((ed) => ed.includes(domain)).flat();
return new Set(equivalents);
}),
);
return domains$;
}
}

View File

@ -0,0 +1,24 @@
/*
See full documentation at:
https://bitwarden.com/help/uri-match-detection/#match-detection-options
Domain: "the top-level domain and second-level domain of the URI match the detected resource",
Host: "the hostname and (if specified) port of the URI matches the detected resource",
StartsWith: "the detected resource starts with the URI, regardless of what follows it",
Exact: "the URI matches the detected resource exactly",
RegularExpression: "the detected resource matches a specified regular expression",
Never: "never offer auto-fill for the item",
*/
export const UriMatchStrategy = {
Domain: 0,
Host: 1,
StartsWith: 2,
Exact: 3,
RegularExpression: 4,
Never: 5,
} as const;
export type UriMatchStrategySetting = (typeof UriMatchStrategy)[keyof typeof UriMatchStrategy];
export type NeverDomains = { [id: string]: unknown };
export type EquivalentDomains = string[][];

View File

@ -1,5 +1,5 @@
import { UriMatchStrategySetting } from "../../models/domain/domain-service";
import { EncString } from "../../platform/models/domain/enc-string"; import { EncString } from "../../platform/models/domain/enc-string";
import { UriMatchType } from "../../vault/enums";
import { LoginUri as LoginUriDomain } from "../../vault/models/domain/login-uri"; import { LoginUri as LoginUriDomain } from "../../vault/models/domain/login-uri";
import { LoginUriView } from "../../vault/models/view/login-uri.view"; import { LoginUriView } from "../../vault/models/view/login-uri.view";
@ -26,7 +26,7 @@ export class LoginUriExport {
uri: string; uri: string;
uriChecksum: string | undefined; uriChecksum: string | undefined;
match: UriMatchType = null; match: UriMatchStrategySetting = null;
constructor(o?: LoginUriView | LoginUriDomain) { constructor(o?: LoginUriView | LoginUriDomain) {
if (o == null) { if (o == null) {

View File

@ -1,4 +1,8 @@
import { Observable } from "rxjs";
export abstract class AppIdService { export abstract class AppIdService {
appId$: Observable<string>;
anonymousAppId$: Observable<string>;
getAppId: () => Promise<string>; getAppId: () => Promise<string>;
getAnonymousAppId: () => Promise<string>; getAnonymousAppId: () => Promise<string>;
} }

View File

@ -14,17 +14,12 @@ import { SendData } from "../../tools/send/models/data/send.data";
import { SendView } from "../../tools/send/models/view/send.view"; import { SendView } from "../../tools/send/models/view/send.view";
import { UserId } from "../../types/guid"; import { UserId } from "../../types/guid";
import { DeviceKey, MasterKey } from "../../types/key"; import { DeviceKey, MasterKey } from "../../types/key";
import { UriMatchType } from "../../vault/enums";
import { CipherData } from "../../vault/models/data/cipher.data"; import { CipherData } from "../../vault/models/data/cipher.data";
import { LocalData } from "../../vault/models/data/local.data"; import { LocalData } from "../../vault/models/data/local.data";
import { CipherView } from "../../vault/models/view/cipher.view"; import { CipherView } from "../../vault/models/view/cipher.view";
import { KdfType, ThemeType } from "../enums"; import { KdfType, ThemeType } from "../enums";
import { ServerConfigData } from "../models/data/server-config.data"; import { ServerConfigData } from "../models/data/server-config.data";
import { import { Account, AccountDecryptionOptions } from "../models/domain/account";
Account,
AccountDecryptionOptions,
AccountSettingsSettings,
} from "../models/domain/account";
import { EncString } from "../models/domain/enc-string"; import { EncString } from "../models/domain/enc-string";
import { StorageOptions } from "../models/domain/storage-options"; import { StorageOptions } from "../models/domain/storage-options";
import { SymmetricCryptoKey } from "../models/domain/symmetric-crypto-key"; import { SymmetricCryptoKey } from "../models/domain/symmetric-crypto-key";
@ -184,8 +179,6 @@ export abstract class StateService<T extends Account = Account> {
* @deprecated Do not call this directly, use SendService * @deprecated Do not call this directly, use SendService
*/ */
setDecryptedSends: (value: SendView[], options?: StorageOptions) => Promise<void>; setDecryptedSends: (value: SendView[], options?: StorageOptions) => Promise<void>;
getDefaultUriMatch: (options?: StorageOptions) => Promise<UriMatchType>;
setDefaultUriMatch: (value: UriMatchType, options?: StorageOptions) => Promise<void>;
/** /**
* @deprecated Do not call this, use SettingsService * @deprecated Do not call this, use SettingsService
*/ */
@ -272,8 +265,6 @@ export abstract class StateService<T extends Account = Account> {
* @deprecated Do not call this directly, use SendService * @deprecated Do not call this directly, use SendService
*/ */
setEncryptedSends: (value: { [id: string]: SendData }, options?: StorageOptions) => Promise<void>; setEncryptedSends: (value: { [id: string]: SendData }, options?: StorageOptions) => Promise<void>;
getEquivalentDomains: (options?: StorageOptions) => Promise<string[][]>;
setEquivalentDomains: (value: string, options?: StorageOptions) => Promise<void>;
getEventCollection: (options?: StorageOptions) => Promise<EventData[]>; getEventCollection: (options?: StorageOptions) => Promise<EventData[]>;
setEventCollection: (value: EventData[], options?: StorageOptions) => Promise<void>; setEventCollection: (value: EventData[], options?: StorageOptions) => Promise<void>;
getEverBeenUnlocked: (options?: StorageOptions) => Promise<boolean>; getEverBeenUnlocked: (options?: StorageOptions) => Promise<boolean>;
@ -307,8 +298,6 @@ export abstract class StateService<T extends Account = Account> {
setMainWindowSize: (value: number, options?: StorageOptions) => Promise<void>; setMainWindowSize: (value: number, options?: StorageOptions) => Promise<void>;
getMinimizeOnCopyToClipboard: (options?: StorageOptions) => Promise<boolean>; getMinimizeOnCopyToClipboard: (options?: StorageOptions) => Promise<boolean>;
setMinimizeOnCopyToClipboard: (value: boolean, options?: StorageOptions) => Promise<void>; setMinimizeOnCopyToClipboard: (value: boolean, options?: StorageOptions) => Promise<void>;
getNeverDomains: (options?: StorageOptions) => Promise<{ [id: string]: unknown }>;
setNeverDomains: (value: { [id: string]: unknown }, options?: StorageOptions) => Promise<void>;
getOpenAtLogin: (options?: StorageOptions) => Promise<boolean>; getOpenAtLogin: (options?: StorageOptions) => Promise<boolean>;
setOpenAtLogin: (value: boolean, options?: StorageOptions) => Promise<void>; setOpenAtLogin: (value: boolean, options?: StorageOptions) => Promise<void>;
getOrganizationInvitation: (options?: StorageOptions) => Promise<any>; getOrganizationInvitation: (options?: StorageOptions) => Promise<any>;
@ -350,14 +339,6 @@ export abstract class StateService<T extends Account = Account> {
setRememberedEmail: (value: string, options?: StorageOptions) => Promise<void>; setRememberedEmail: (value: string, options?: StorageOptions) => Promise<void>;
getSecurityStamp: (options?: StorageOptions) => Promise<string>; getSecurityStamp: (options?: StorageOptions) => Promise<string>;
setSecurityStamp: (value: string, options?: StorageOptions) => Promise<void>; setSecurityStamp: (value: string, options?: StorageOptions) => Promise<void>;
/**
* @deprecated Do not call this directly, use SettingsService
*/
getSettings: (options?: StorageOptions) => Promise<AccountSettingsSettings>;
/**
* @deprecated Do not call this directly, use SettingsService
*/
setSettings: (value: AccountSettingsSettings, options?: StorageOptions) => Promise<void>;
getTheme: (options?: StorageOptions) => Promise<ThemeType>; getTheme: (options?: StorageOptions) => Promise<ThemeType>;
setTheme: (value: ThemeType, options?: StorageOptions) => Promise<void>; setTheme: (value: ThemeType, options?: StorageOptions) => Promise<void>;
getTwoFactorToken: (options?: StorageOptions) => Promise<string>; getTwoFactorToken: (options?: StorageOptions) => Promise<string>;

View File

@ -253,11 +253,10 @@ export class Utils {
}); });
} }
static guidRegex = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/;
static isGuid(id: string) { static isGuid(id: string) {
return RegExp( return RegExp(Utils.guidRegex, "i").test(id);
/^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/,
"i",
).test(id);
} }
static getHostname(uriString: string): string { static getHostname(uriString: string): string {

View File

@ -7,6 +7,7 @@ import { KeyConnectorUserDecryptionOption } from "../../../auth/models/domain/us
import { TrustedDeviceUserDecryptionOption } from "../../../auth/models/domain/user-decryption-options/trusted-device-user-decryption-option"; import { TrustedDeviceUserDecryptionOption } from "../../../auth/models/domain/user-decryption-options/trusted-device-user-decryption-option";
import { IdentityTokenResponse } from "../../../auth/models/response/identity-token.response"; import { IdentityTokenResponse } from "../../../auth/models/response/identity-token.response";
import { EventData } from "../../../models/data/event.data"; import { EventData } from "../../../models/data/event.data";
import { UriMatchStrategySetting } from "../../../models/domain/domain-service";
import { GeneratorOptions } from "../../../tools/generator/generator-options"; import { GeneratorOptions } from "../../../tools/generator/generator-options";
import { import {
GeneratedPasswordHistory, GeneratedPasswordHistory,
@ -17,7 +18,6 @@ import { SendData } from "../../../tools/send/models/data/send.data";
import { SendView } from "../../../tools/send/models/view/send.view"; import { SendView } from "../../../tools/send/models/view/send.view";
import { DeepJsonify } from "../../../types/deep-jsonify"; import { DeepJsonify } from "../../../types/deep-jsonify";
import { MasterKey } from "../../../types/key"; import { MasterKey } from "../../../types/key";
import { UriMatchType } from "../../../vault/enums";
import { CipherData } from "../../../vault/models/data/cipher.data"; import { CipherData } from "../../../vault/models/data/cipher.data";
import { CipherView } from "../../../vault/models/view/cipher.view"; import { CipherView } from "../../../vault/models/view/cipher.view";
import { AddEditCipherInfo } from "../../../vault/types/add-edit-cipher-info"; import { AddEditCipherInfo } from "../../../vault/types/add-edit-cipher-info";
@ -196,13 +196,12 @@ export class AccountProfile {
export class AccountSettings { export class AccountSettings {
autoConfirmFingerPrints?: boolean; autoConfirmFingerPrints?: boolean;
defaultUriMatch?: UriMatchType; defaultUriMatch?: UriMatchStrategySetting;
disableGa?: boolean; disableGa?: boolean;
dontShowCardsCurrentTab?: boolean; dontShowCardsCurrentTab?: boolean;
dontShowIdentitiesCurrentTab?: boolean; dontShowIdentitiesCurrentTab?: boolean;
enableAlwaysOnTop?: boolean; enableAlwaysOnTop?: boolean;
enableBiometric?: boolean; enableBiometric?: boolean;
equivalentDomains?: any;
minimizeOnCopyToClipboard?: boolean; minimizeOnCopyToClipboard?: boolean;
passwordGenerationOptions?: PasswordGeneratorOptions; passwordGenerationOptions?: PasswordGeneratorOptions;
usernameGenerationOptions?: UsernameGeneratorOptions; usernameGenerationOptions?: UsernameGeneratorOptions;
@ -210,7 +209,6 @@ export class AccountSettings {
pinKeyEncryptedUserKey?: EncryptedString; pinKeyEncryptedUserKey?: EncryptedString;
pinKeyEncryptedUserKeyEphemeral?: EncryptedString; pinKeyEncryptedUserKeyEphemeral?: EncryptedString;
protectedPin?: string; protectedPin?: string;
settings?: AccountSettingsSettings; // TODO: Merge whatever is going on here into the AccountSettings model properly
vaultTimeout?: number; vaultTimeout?: number;
vaultTimeoutAction?: string = "lock"; vaultTimeoutAction?: string = "lock";
serverConfig?: ServerConfigData; serverConfig?: ServerConfigData;
@ -236,10 +234,6 @@ export class AccountSettings {
} }
} }
export type AccountSettingsSettings = {
equivalentDomains?: string[][];
};
export class AccountTokens { export class AccountTokens {
accessToken?: string; accessToken?: string;
refreshToken?: string; refreshToken?: string;

View File

@ -25,6 +25,5 @@ export class GlobalState {
enableBrowserIntegration?: boolean; enableBrowserIntegration?: boolean;
enableBrowserIntegrationFingerprint?: boolean; enableBrowserIntegrationFingerprint?: boolean;
enableDuckDuckGoBrowserIntegration?: boolean; enableDuckDuckGoBrowserIntegration?: boolean;
neverDomains?: { [id: string]: unknown };
deepLinkRedirectUrl?: string; deepLinkRedirectUrl?: string;
} }

View File

@ -0,0 +1,101 @@
import { FakeGlobalStateProvider } from "../../../spec";
import { Utils } from "../misc/utils";
import { ANONYMOUS_APP_ID_KEY, APP_ID_KEY, AppIdService } from "./app-id.service";
describe("AppIdService", () => {
const globalStateProvider = new FakeGlobalStateProvider();
const appIdState = globalStateProvider.getFake(APP_ID_KEY);
const anonymousAppIdState = globalStateProvider.getFake(ANONYMOUS_APP_ID_KEY);
let sut: AppIdService;
beforeEach(() => {
sut = new AppIdService(globalStateProvider);
});
afterEach(() => {
jest.resetAllMocks();
});
describe("getAppId", () => {
it("returns the existing appId when it exists", async () => {
appIdState.stateSubject.next("existingAppId");
const appId = await sut.getAppId();
expect(appId).toBe("existingAppId");
});
it.each([null, undefined])(
"uses the util function to create a new id when it AppId does not exist",
async (value) => {
appIdState.stateSubject.next(value);
const spy = jest.spyOn(Utils, "newGuid");
await sut.getAppId();
expect(spy).toHaveBeenCalledTimes(1);
},
);
it.each([null, undefined])("returns a new appId when it does not exist", async (value) => {
appIdState.stateSubject.next(value);
const appId = await sut.getAppId();
expect(appId).toMatch(Utils.guidRegex);
});
it.each([null, undefined])(
"stores the new guid when it an existing one is not found",
async (value) => {
appIdState.stateSubject.next(value);
const appId = await sut.getAppId();
expect(appIdState.nextMock).toHaveBeenCalledWith(appId);
},
);
});
describe("getAnonymousAppId", () => {
it("returns the existing appId when it exists", async () => {
anonymousAppIdState.stateSubject.next("existingAppId");
const appId = await sut.getAnonymousAppId();
expect(appId).toBe("existingAppId");
});
it.each([null, undefined])(
"uses the util function to create a new id when it AppId does not exist",
async (value) => {
anonymousAppIdState.stateSubject.next(value);
const spy = jest.spyOn(Utils, "newGuid");
await sut.getAnonymousAppId();
expect(spy).toHaveBeenCalledTimes(1);
},
);
it.each([null, undefined])("returns a new appId when it does not exist", async (value) => {
anonymousAppIdState.stateSubject.next(value);
const appId = await sut.getAnonymousAppId();
expect(appId).toMatch(Utils.guidRegex);
});
it.each([null, undefined])(
"stores the new guid when it an existing one is not found",
async (value) => {
anonymousAppIdState.stateSubject.next(value);
const appId = await sut.getAnonymousAppId();
expect(anonymousAppIdState.nextMock).toHaveBeenCalledWith(appId);
},
);
});
});

View File

@ -1,31 +1,46 @@
import { Observable, filter, firstValueFrom, tap } from "rxjs";
import { AppIdService as AppIdServiceAbstraction } from "../abstractions/app-id.service"; import { AppIdService as AppIdServiceAbstraction } from "../abstractions/app-id.service";
import { AbstractStorageService } from "../abstractions/storage.service";
import { HtmlStorageLocation } from "../enums";
import { Utils } from "../misc/utils"; import { Utils } from "../misc/utils";
import { APPLICATION_ID_DISK, GlobalStateProvider, KeyDefinition } from "../state";
export const APP_ID_KEY = new KeyDefinition(APPLICATION_ID_DISK, "appId", {
deserializer: (value: string) => value,
});
export const ANONYMOUS_APP_ID_KEY = new KeyDefinition(APPLICATION_ID_DISK, "anonymousAppId", {
deserializer: (value: string) => value,
});
export class AppIdService implements AppIdServiceAbstraction { export class AppIdService implements AppIdServiceAbstraction {
constructor(private storageService: AbstractStorageService) {} appId$: Observable<string>;
anonymousAppId$: Observable<string>;
getAppId(): Promise<string> { constructor(globalStateProvider: GlobalStateProvider) {
return this.makeAndGetAppId("appId"); const appIdState = globalStateProvider.get(APP_ID_KEY);
const anonymousAppIdState = globalStateProvider.get(ANONYMOUS_APP_ID_KEY);
this.appId$ = appIdState.state$.pipe(
tap(async (appId) => {
if (!appId) {
await appIdState.update(() => Utils.newGuid());
}
}),
filter((appId) => !!appId),
);
this.anonymousAppId$ = anonymousAppIdState.state$.pipe(
tap(async (appId) => {
if (!appId) {
await anonymousAppIdState.update(() => Utils.newGuid());
}
}),
filter((appId) => !!appId),
);
} }
getAnonymousAppId(): Promise<string> { async getAppId(): Promise<string> {
return this.makeAndGetAppId("anonymousAppId"); return await firstValueFrom(this.appId$);
} }
private async makeAndGetAppId(key: string) { async getAnonymousAppId(): Promise<string> {
const existingId = await this.storageService.get<string>(key, { return await firstValueFrom(this.anonymousAppId$);
htmlStorageLocation: HtmlStorageLocation.Local,
});
if (existingId != null) {
return existingId;
}
const guid = Utils.newGuid();
await this.storageService.save(key, guid, {
htmlStorageLocation: HtmlStorageLocation.Local,
});
return guid;
} }
} }

View File

@ -18,7 +18,6 @@ import { SendData } from "../../tools/send/models/data/send.data";
import { SendView } from "../../tools/send/models/view/send.view"; import { SendView } from "../../tools/send/models/view/send.view";
import { UserId } from "../../types/guid"; import { UserId } from "../../types/guid";
import { DeviceKey, MasterKey } from "../../types/key"; import { DeviceKey, MasterKey } from "../../types/key";
import { UriMatchType } from "../../vault/enums";
import { CipherData } from "../../vault/models/data/cipher.data"; import { CipherData } from "../../vault/models/data/cipher.data";
import { LocalData } from "../../vault/models/data/local.data"; import { LocalData } from "../../vault/models/data/local.data";
import { CipherView } from "../../vault/models/view/cipher.view"; import { CipherView } from "../../vault/models/view/cipher.view";
@ -41,7 +40,6 @@ import {
AccountData, AccountData,
AccountDecryptionOptions, AccountDecryptionOptions,
AccountSettings, AccountSettings,
AccountSettingsSettings,
} from "../models/domain/account"; } from "../models/domain/account";
import { EncString } from "../models/domain/enc-string"; import { EncString } from "../models/domain/enc-string";
import { GlobalState } from "../models/domain/global-state"; import { GlobalState } from "../models/domain/global-state";
@ -786,23 +784,6 @@ export class StateService<
); );
} }
async getDefaultUriMatch(options?: StorageOptions): Promise<UriMatchType> {
return (
await this.getAccount(this.reconcileOptions(options, await this.defaultOnDiskOptions()))
)?.settings?.defaultUriMatch;
}
async setDefaultUriMatch(value: UriMatchType, options?: StorageOptions): Promise<void> {
const account = await this.getAccount(
this.reconcileOptions(options, await this.defaultOnDiskOptions()),
);
account.settings.defaultUriMatch = value;
await this.saveAccount(
account,
this.reconcileOptions(options, await this.defaultOnDiskOptions()),
);
}
async getDisableFavicon(options?: StorageOptions): Promise<boolean> { async getDisableFavicon(options?: StorageOptions): Promise<boolean> {
return ( return (
( (
@ -1304,23 +1285,6 @@ export class StateService<
); );
} }
async getEquivalentDomains(options?: StorageOptions): Promise<string[][]> {
return (
await this.getAccount(this.reconcileOptions(options, await this.defaultOnDiskOptions()))
)?.settings?.equivalentDomains;
}
async setEquivalentDomains(value: string, options?: StorageOptions): Promise<void> {
const account = await this.getAccount(
this.reconcileOptions(options, await this.defaultOnDiskOptions()),
);
account.settings.equivalentDomains = value;
await this.saveAccount(
account,
this.reconcileOptions(options, await this.defaultOnDiskOptions()),
);
}
@withPrototypeForArrayMembers(EventData) @withPrototypeForArrayMembers(EventData)
async getEventCollection(options?: StorageOptions): Promise<EventData[]> { async getEventCollection(options?: StorageOptions): Promise<EventData[]> {
return ( return (
@ -1580,23 +1544,6 @@ export class StateService<
); );
} }
async getNeverDomains(options?: StorageOptions): Promise<{ [id: string]: unknown }> {
return (
await this.getGlobals(this.reconcileOptions(options, await this.defaultOnDiskOptions()))
)?.neverDomains;
}
async setNeverDomains(value: { [id: string]: unknown }, options?: StorageOptions): Promise<void> {
const globals = await this.getGlobals(
this.reconcileOptions(options, await this.defaultOnDiskOptions()),
);
globals.neverDomains = value;
await this.saveGlobals(
globals,
this.reconcileOptions(options, await this.defaultOnDiskOptions()),
);
}
async getOpenAtLogin(options?: StorageOptions): Promise<boolean> { async getOpenAtLogin(options?: StorageOptions): Promise<boolean> {
return ( return (
(await this.getGlobals(this.reconcileOptions(options, await this.defaultOnDiskOptions()))) (await this.getGlobals(this.reconcileOptions(options, await this.defaultOnDiskOptions())))
@ -1778,23 +1725,6 @@ export class StateService<
); );
} }
async getSettings(options?: StorageOptions): Promise<AccountSettingsSettings> {
return (
await this.getAccount(this.reconcileOptions(options, await this.defaultOnDiskMemoryOptions()))
)?.settings?.settings;
}
async setSettings(value: AccountSettingsSettings, options?: StorageOptions): Promise<void> {
const account = await this.getAccount(
this.reconcileOptions(options, await this.defaultOnDiskMemoryOptions()),
);
account.settings.settings = value;
await this.saveAccount(
account,
this.reconcileOptions(options, await this.defaultOnDiskMemoryOptions()),
);
}
async getTheme(options?: StorageOptions): Promise<ThemeType> { async getTheme(options?: StorageOptions): Promise<ThemeType> {
return ( return (
await this.getGlobals(this.reconcileOptions(options, await this.defaultOnDiskLocalOptions())) await this.getGlobals(this.reconcileOptions(options, await this.defaultOnDiskLocalOptions()))

View File

@ -15,6 +15,7 @@ export interface GlobalState<T> {
* @param options.combineLatestWith An observable that you want to combine with the current state for callbacks. Defaults to null * @param options.combineLatestWith An observable that you want to combine with the current state for callbacks. Defaults to null
* @param options.msTimeout A timeout for how long you are willing to wait for a `combineLatestWith` option to complete. Defaults to 1000ms. Only applies if `combineLatestWith` is set. * @param options.msTimeout A timeout for how long you are willing to wait for a `combineLatestWith` option to complete. Defaults to 1000ms. Only applies if `combineLatestWith` is set.
* @returns A promise that must be awaited before your next action to ensure the update has been written to state. * @returns A promise that must be awaited before your next action to ensure the update has been written to state.
* Resolves to the new state. If `shouldUpdate` returns false, the promise will resolve to the current state.
*/ */
update: <TCombine>( update: <TCombine>(
configureState: (state: T, dependency: TCombine) => T, configureState: (state: T, dependency: TCombine) => T,

View File

@ -27,6 +27,7 @@ export const PROVIDERS_DISK = new StateDefinition("providers", "disk");
export const ACCOUNT_MEMORY = new StateDefinition("account", "memory"); export const ACCOUNT_MEMORY = new StateDefinition("account", "memory");
export const SSO_DISK = new StateDefinition("ssoLogin", "disk"); export const SSO_DISK = new StateDefinition("ssoLogin", "disk");
export const LOGIN_STRATEGY_MEMORY = new StateDefinition("loginStrategy", "memory");
// Autofill // Autofill
@ -38,6 +39,8 @@ export const USER_NOTIFICATION_SETTINGS_DISK = new StateDefinition(
// Billing // Billing
export const DOMAIN_SETTINGS_DISK = new StateDefinition("domainSettings", "disk");
export const AUTOFILL_SETTINGS_DISK = new StateDefinition("autofillSettings", "disk"); export const AUTOFILL_SETTINGS_DISK = new StateDefinition("autofillSettings", "disk");
export const AUTOFILL_SETTINGS_DISK_LOCAL = new StateDefinition("autofillSettingsLocal", "disk", { export const AUTOFILL_SETTINGS_DISK_LOCAL = new StateDefinition("autofillSettingsLocal", "disk", {
web: "disk-local", web: "disk-local",
@ -52,6 +55,9 @@ export const NEW_WEB_LAYOUT_BANNER_DISK = new StateDefinition("newWebLayoutBanne
// Platform // Platform
export const APPLICATION_ID_DISK = new StateDefinition("applicationId", "disk", {
web: "disk-local",
});
export const BIOMETRIC_SETTINGS_DISK = new StateDefinition("biometricSettings", "disk"); export const BIOMETRIC_SETTINGS_DISK = new StateDefinition("biometricSettings", "disk");
export const CLEAR_EVENT_DISK = new StateDefinition("clearEvent", "disk"); export const CLEAR_EVENT_DISK = new StateDefinition("clearEvent", "disk");
export const CRYPTO_DISK = new StateDefinition("crypto", "disk"); export const CRYPTO_DISK = new StateDefinition("crypto", "disk");

View File

@ -32,7 +32,8 @@ export interface ActiveUserState<T> extends UserState<T> {
* @param options.combineLatestWith An observable that you want to combine with the current state for callbacks. Defaults to null * @param options.combineLatestWith An observable that you want to combine with the current state for callbacks. Defaults to null
* @param options.msTimeout A timeout for how long you are willing to wait for a `combineLatestWith` option to complete. Defaults to 1000ms. Only applies if `combineLatestWith` is set. * @param options.msTimeout A timeout for how long you are willing to wait for a `combineLatestWith` option to complete. Defaults to 1000ms. Only applies if `combineLatestWith` is set.
* @returns The new state * @returns A promise that must be awaited before your next action to ensure the update has been written to state.
* Resolves to the new state. If `shouldUpdate` returns false, the promise will resolve to the current state.
*/ */
readonly update: <TCombine>( readonly update: <TCombine>(
configureState: (state: T, dependencies: TCombine) => T, configureState: (state: T, dependencies: TCombine) => T,
@ -50,7 +51,8 @@ export interface SingleUserState<T> extends UserState<T> {
* @param options.combineLatestWith An observable that you want to combine with the current state for callbacks. Defaults to null * @param options.combineLatestWith An observable that you want to combine with the current state for callbacks. Defaults to null
* @param options.msTimeout A timeout for how long you are willing to wait for a `combineLatestWith` option to complete. Defaults to 1000ms. Only applies if `combineLatestWith` is set. * @param options.msTimeout A timeout for how long you are willing to wait for a `combineLatestWith` option to complete. Defaults to 1000ms. Only applies if `combineLatestWith` is set.
* @returns The new state * @returns A promise that must be awaited before your next action to ensure the update has been written to state.
* Resolves to the new state. If `shouldUpdate` returns false, the promise will resolve to the current state.
*/ */
readonly update: <TCombine>( readonly update: <TCombine>(
configureState: (state: T, dependencies: TCombine) => T, configureState: (state: T, dependencies: TCombine) => T,

Some files were not shown because too many files have changed in this diff Show More