Compare commits

...

36 Commits

Author SHA1 Message Date
nick-livefront d748d006b8
Merge branch 'main' of https://github.com/bitwarden/clients into pm-7231-product-switcher-navigation 2024-04-26 16:39:10 -05:00
nick-livefront 6e49eeaf09
apply margin-top via class on the host component 2024-04-26 16:38:36 -05:00
nick-livefront 68792b81b3
move observables to protected readonly 2024-04-26 16:34:15 -05:00
nick-livefront 749fcdfb33
refactor `navigationUI` to `otherProductOverrides` 2024-04-26 16:30:57 -05:00
nick-livefront bd54d8713d
update to satisfies 2024-04-26 16:24:45 -05:00
nick-livefront 7c622e8e59
only use wrapping div in navigation switcher story
- less vertical space is taken up
2024-04-26 16:22:17 -05:00
nick-livefront 6dd95d60f6
remove double subscription to `moreProducts$` 2024-04-26 16:16:25 -05:00
nick-livefront dafaf01b39
migrate stories to CSF3 2024-04-26 15:00:11 -05:00
Cesar Gonzalez 5dc200577c
[PM-7663] Update Build Pipeline for Beta Labelling (#8903)
* [PM-7663] Update build pipeline for beta labeling

* [PM-7663] Update build pipeline for beta labelling

* [PM-7663] Update build pipeline for beta labelling

* [PM-7663] Update build pipeline for beta labelling

* [PM-7663] Update build pipeline for beta labelling

* [PM-7663] Incorporate build workflow for the Chrome manifest v3 beta

* [PM-7663] Update build pipeline for beta labeling

* [PM-7663] Update build pipeline for beta labeling

* [PM-7663] Update build pipeline for beta labeling

* [PM-7663] Ensure we can have a valid version number based on the github run id

* [PM-7663] Ensure we can have a valid version number based on the github run id

* [PM-7663] Reverting change made to the run id, as it will not function

* [PM-7663] Reverting change made to the run id, as it will not function

* [PM-7663] Reverting change made to the run id, as it will not function

* [PM-7663] Reverting change made to the run id, as it will not function

* [PM-7663] Reverting a typo

* Fix Duplicate `process.env

* Learn how to use

---------

Co-authored-by: Justin Baur <19896123+justindbaur@users.noreply.github.com>
2024-04-26 15:15:36 -04:00
Justin Baur a8e4366ec0
Check that `self` is undefined instead of `window` (#8940) 2024-04-26 15:08:59 -04:00
Matt Gibson 089f251a0c
Remove memory storage cache from derived state. Use observable cache and port messaging (#8939) 2024-04-26 15:08:39 -04:00
renovate[bot] b3242145f9
[deps] Platform (CL): Update autoprefixer to v10.4.19 (#8735)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-04-26 14:59:15 -04:00
Justin Baur b482a15d34
Bandaid Folders Not Emitting (#8934)
* Bandaid Folders Not Emitting

* Remove VaultFilterComponent Change
2024-04-26 14:41:57 -04:00
nick-livefront 7714118116
use protected readonly variable instead of getter 2024-04-26 13:22:08 -05:00
nick-livefront 5a5f9ee147
remove unneeded full width style 2024-04-26 13:15:46 -05:00
nick-livefront 5c27e8dc84
Merge branch 'main' of https://github.com/bitwarden/clients into pm-7231-product-switcher-navigation 2024-04-26 13:05:15 -05:00
Matt Gibson a4f1a3f13d
Use unique port names for derived states (#8938) 2024-04-26 16:17:34 +00:00
Matt Gibson 4c1c09f07f
Use unique port names for derived states (#8937) 2024-04-26 11:21:42 -04:00
Robyn MacCallum 67280f48dd
Make integrations links open in a new page (#8933)
* Make integrations links open in a new page

* Fix target warning
2024-04-26 10:53:11 -04:00
Justin Baur a7958c1a56
Display `version_name` in AboutComponent (#8931) 2024-04-26 10:23:11 -04:00
Jared Snider 7f5efcc18c
PM-7745 - SSO Login Strategy - trySetUserKeyWithDeviceKey should use the user id from the IdTokenResponse and not StateService as I suspect it's not working as expected. Thinking there might be a race condition where the user id is null or maybe incorrect. (#8927) 2024-04-26 10:18:05 -04:00
Justin Baur acea273f97
Fix ConfigService Merge Conflict (#8932) 2024-04-26 09:39:46 -04:00
Victoria League ec37e5e4d3
[CL-219][CL-218][CL-217] Add new extension layout components (#8728) 2024-04-26 09:35:32 -04:00
KiruthigaManivannan 2fa4c6e4f9
PM-4945 Update Two Factor verify dialog (#8580)
* PM-4945 Update Two Factor verify dialog

* PM-4945 Addressed review comments

* PM-4945 Removed legacy User verification component and used new one
2024-04-26 18:24:48 +05:30
github-actions[bot] 11ba8d188d
Autosync the updated translations (#8925)
Co-authored-by: bitwarden-devops-bot <106330231+bitwarden-devops-bot@users.noreply.github.com>
2024-04-26 11:06:19 +00:00
Oscar Hinton 14b2eb99a2
[PM-2282] Make feature flags type safe (#8612)
Refactors the feature flags in ConfigService to be type safe. It also moves the default value to a centralized location rather than the caller defining it. This ensures consistency across the various places they are used.
2024-04-26 12:57:26 +02:00
github-actions[bot] c7fa376be3
Autosync the updated translations (#8926)
Co-authored-by: bitwarden-devops-bot <106330231+bitwarden-devops-bot@users.noreply.github.com>
2024-04-26 07:05:43 +00:00
github-actions[bot] 788bef6b7a
Autosync the updated translations (#8924)
Co-authored-by: bitwarden-devops-bot <106330231+bitwarden-devops-bot@users.noreply.github.com>
2024-04-26 07:04:21 +00:00
Thomas Rittson c21a58f2fb
Use refCount: true to avoid potential memory leak (#8796) 2024-04-26 08:36:57 +10:00
Cesar Gonzalez c3d4c7aa3d
[PM-7710] Avoid re-indexing ciphers on current tab component and re-setting null storage values for popup components (#8908)
* [PM-7710] Avoid re-indexing ciphers on current tab component and re-setting null storage values for popup components

* [PM-7710] Avoid re-indexing ciphers on current tab component and re-setting null storage values for popup components
2024-04-25 16:47:20 -05:00
Jake Fink 2ff3fa92fb
[PM-7702] Remove extra content script being injected (#8922)
* remove extra content script being injected that results in multiple messages

* add conditional logic for when to add script
2024-04-25 17:27:43 -04:00
SmithThe4th d8749a0c56
[AC-2359] Ownership does not default to an organization when Remove Individual Vault policy is active (#8910)
* fixed issue with clearing search index state

* clear user index before account is totally cleaned up

* added logout clear on option

* removed redundant clear index from logout

* fixed ownsership dropdown issu where async operations does bot complete early enough before the view is shown
2024-04-25 16:58:25 -04:00
Jake Fink 8afe915be1
[PM-7564] Move 2fa and login strategy service to popup and add state providers to 2fa service (#8820)
* remove 2fa from main.background

* remove login strategy service from main.background

* move 2fa and login strategy service to popup, init in browser

* add state providers to 2fa service
- add deserializer helpers

* use key definitions for global state

* fix calls to 2fa service

* remove extra await

* add delay to wait for active account emission in popup

* add and fix tests

* fix cli

* really fix cli

* remove timeout and wait for active account

* verify expected user is active account

* fix tests

* address feedback
2024-04-25 16:45:23 -04:00
Alex Morask cbf7c292f3
[AC-2485] Add redirects to clients components based on FF and provider status (#8839)
* Add provider clients redirects based on FF and provider status

* Fixing broken test
2024-04-25 15:27:06 -04:00
Matt Gibson e516eec200
Reintroduce null object remove rerouting (#8920)
* Reintroduce null object remove rerouting

* Test remove redirect
2024-04-25 14:55:45 -04:00
Ike 1e4158fd87
[PM-5735] Create kdf Service (#8715)
* key connector migration initial

* migrator complete

* fix dependencies

* finalized tests

* fix deps and sync main

* clean up definition file

* fixing tests

* fixed tests

* fixing CLI, Browser, Desktop builds

* fixed factory options

* reverting exports

* implemented UserKeyDefinition clearOn

* Initial Kdf Service Changes

* rename and account setting kdfconfig

* fixing tests and renaming migration

* fixed DI ordering for browser

* rename and fix DI

* Clean up Migrations

* fixing migrations

* begin data structure changes for kdf config

* Make KDF more type safe; co-author: jlf0dev

* fixing tests

* Fixed CLI login and comments

* set now accepts userId and test updates

---------

Co-authored-by: Jake Fink <jfink@bitwarden.com>
2024-04-25 11:26:01 -07:00
218 changed files with 2681 additions and 1410 deletions

View File

@ -164,6 +164,10 @@ jobs:
run: npm run dist:mv3
working-directory: browser-source/apps/browser
- name: Build Chrome Manifest v3 Beta
run: npm run dist:chrome:beta
working-directory: browser-source/apps/browser
- name: Gulp
run: gulp ci
working-directory: browser-source/apps/browser
@ -196,6 +200,13 @@ jobs:
path: browser-source/apps/browser/dist/dist-chrome-mv3.zip
if-no-files-found: error
- name: Upload Chrome MV3 Beta artifact (DO NOT USE FOR PROD)
uses: actions/upload-artifact@5d5d22a31266ced268874388b861e4b58bb5c2f3 # v4.3.1
with:
name: DO-NOT-USE-FOR-PROD-dist-chrome-MV3-beta-${{ env._BUILD_NUMBER }}.zip
path: browser-source/apps/browser/dist/dist-chrome-mv3-beta.zip
if-no-files-found: error
- name: Upload Firefox artifact
uses: actions/upload-artifact@5d5d22a31266ced268874388b861e4b58bb5c2f3 # v4.3.1
with:

View File

@ -9,6 +9,8 @@ const config: StorybookConfig = {
"../libs/components/src/**/*.stories.@(js|jsx|ts|tsx)",
"../apps/web/src/**/*.mdx",
"../apps/web/src/**/*.stories.@(js|jsx|ts|tsx)",
"../apps/browser/src/**/*.mdx",
"../apps/browser/src/**/*.stories.@(js|jsx|ts|tsx)",
"../bitwarden_license/bit-web/src/**/*.mdx",
"../bitwarden_license/bit-web/src/**/*.stories.@(js|jsx|ts|tsx)",
],

View File

@ -1,12 +1,10 @@
{
"extends": "../tsconfig",
"compilerOptions": {
"types": ["node", "jest", "chrome"],
"allowSyntheticDefaultImports": true
},
"exclude": ["../src/test.setup.ts", "../apps/src/**/*.spec.ts", "../libs/**/*.spec.ts"],
"exclude": ["../src/test.setup.ts", "../apps/**/*.spec.ts", "../libs/**/*.spec.ts"],
"files": [
"./typings.d.ts",
"./preview.tsx",
"../libs/components/src/main.ts",
"../libs/components/src/polyfills.ts"

View File

@ -1,4 +0,0 @@
declare module "*.md" {
const content: string;
export default content;
}

View File

@ -35,6 +35,9 @@ function buildString() {
if (process.env.MANIFEST_VERSION) {
build = `-mv${process.env.MANIFEST_VERSION}`;
}
if (process.env.BETA_BUILD === "1") {
build += "-beta";
}
if (process.env.BUILD_NUMBER && process.env.BUILD_NUMBER !== "") {
build = `-${process.env.BUILD_NUMBER}`;
}
@ -65,6 +68,9 @@ function distFirefox() {
manifest.optional_permissions = manifest.optional_permissions.filter(
(permission) => permission !== "privacy",
);
if (process.env.BETA_BUILD === "1") {
manifest = applyBetaLabels(manifest);
}
return manifest;
});
}
@ -72,6 +78,9 @@ function distFirefox() {
function distOpera() {
return dist("opera", (manifest) => {
delete manifest.applications;
if (process.env.BETA_BUILD === "1") {
manifest = applyBetaLabels(manifest);
}
return manifest;
});
}
@ -81,6 +90,9 @@ function distChrome() {
delete manifest.applications;
delete manifest.sidebar_action;
delete manifest.commands._execute_sidebar_action;
if (process.env.BETA_BUILD === "1") {
manifest = applyBetaLabels(manifest);
}
return manifest;
});
}
@ -90,6 +102,9 @@ function distEdge() {
delete manifest.applications;
delete manifest.sidebar_action;
delete manifest.commands._execute_sidebar_action;
if (process.env.BETA_BUILD === "1") {
manifest = applyBetaLabels(manifest);
}
return manifest;
});
}
@ -210,6 +225,9 @@ async function safariCopyBuild(source, dest) {
delete manifest.commands._execute_sidebar_action;
delete manifest.optional_permissions;
manifest.permissions.push("nativeMessaging");
if (process.env.BETA_BUILD === "1") {
manifest = applyBetaLabels(manifest);
}
return manifest;
}),
),
@ -235,6 +253,19 @@ async function ciCoverage(cb) {
.pipe(gulp.dest(paths.coverage));
}
function applyBetaLabels(manifest) {
manifest.name = "Bitwarden Password Manager BETA";
manifest.short_name = "Bitwarden BETA";
manifest.description = "THIS EXTENSION IS FOR BETA TESTING BITWARDEN.";
if (process.env.GITHUB_RUN_ID) {
manifest.version_name = `${manifest.version} beta - ${process.env.GITHUB_SHA.slice(0, 8)}`;
manifest.version = `${manifest.version}.${parseInt(process.env.GITHUB_RUN_ID.slice(-4))}`;
} else {
manifest.version = `${manifest.version}.0`;
}
return manifest;
}
exports["dist:firefox"] = distFirefox;
exports["dist:chrome"] = distChrome;
exports["dist:opera"] = distOpera;

View File

@ -7,10 +7,14 @@
"build:watch": "webpack --watch",
"build:watch:mv3": "cross-env MANIFEST_VERSION=3 webpack --watch",
"build:prod": "cross-env NODE_ENV=production webpack",
"build:prod:beta": "cross-env BETA_BUILD=1 NODE_ENV=production webpack",
"build:prod:watch": "cross-env NODE_ENV=production webpack --watch",
"dist": "npm run build:prod && gulp dist",
"dist:beta": "npm run build:prod:beta && cross-env BETA_BUILD=1 gulp dist",
"dist:mv3": "cross-env MANIFEST_VERSION=3 npm run build:prod && cross-env MANIFEST_VERSION=3 gulp dist",
"dist:mv3:beta": "cross-env MANIFEST_VERSION=3 npm run build:prod:beta && cross-env MANIFEST_VERSION=3 BETA_BUILD=1 gulp dist",
"dist:chrome": "npm run build:prod && gulp dist:chrome",
"dist:chrome:beta": "cross-env MANIFEST_VERSION=3 npm run build:prod:beta && cross-env MANIFEST_VERSION=3 BETA_BUILD=1 gulp dist:chrome",
"dist:firefox": "npm run build:prod && gulp dist:firefox",
"dist:opera": "npm run build:prod && gulp dist:opera",
"dist:safari": "npm run build:prod && gulp dist:safari",

View File

@ -7,7 +7,7 @@
"description": "Extension name, MUST be less than 40 characters (Safari restriction)"
},
"extDesc": {
"message": "At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information",
"message": "W domu, w pracy, lub w ruchu, Bitwarden z łatwością zabezpiecza Twoje hasła, passkeys i poufne informacje",
"description": "Extension description, MUST be less than 112 characters (Safari restriction)"
},
"loginOrCreateNewAccount": {

View File

@ -7,7 +7,7 @@
"description": "Extension name, MUST be less than 40 characters (Safari restriction)"
},
"extDesc": {
"message": "At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information",
"message": "Em qual lugar for, o Bitwarden protege suas senhas, chaves de acesso, e informações confidenciais",
"description": "Extension description, MUST be less than 112 characters (Safari restriction)"
},
"loginOrCreateNewAccount": {

View File

@ -3,11 +3,11 @@
"message": "Bitwarden"
},
"extName": {
"message": "Bitwarden Password Manager",
"message": "Bitwarden - Trình Quản lý Mật khẩu",
"description": "Extension name, MUST be less than 40 characters (Safari restriction)"
},
"extDesc": {
"message": "At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information",
"message": "Ở nhà, ở cơ quan, hay trên đường đi, Bitwarden sẽ bảo mật tất cả mật khẩu, passkey, và thông tin cá nhân của bạn",
"description": "Extension description, MUST be less than 112 characters (Safari restriction)"
},
"loginOrCreateNewAccount": {
@ -650,7 +650,7 @@
"message": "'Thông báo Thêm đăng nhập' sẽ tự động nhắc bạn lưu các đăng nhập mới vào hầm an toàn của bạn bất cứ khi nào bạn đăng nhập trang web lần đầu tiên."
},
"addLoginNotificationDescAlt": {
"message": "Ask to add an item if one isn't found in your vault. Applies to all logged in accounts."
"message": "Đưa ra lựa chọn để thêm một mục nếu không tìm thấy mục đó trong hòm của bạn. Áp dụng với mọi tài khoản đăng nhập trên thiết bị."
},
"showCardsCurrentTab": {
"message": "Hiển thị thẻ trên trang Tab"
@ -685,13 +685,13 @@
"message": "Yêu cầu cập nhật mật khẩu đăng nhập khi phát hiện thay đổi trên trang web."
},
"changedPasswordNotificationDescAlt": {
"message": "Ask to update a login's password when a change is detected on a website. Applies to all logged in accounts."
"message": "Đưa ra lựa chọn để cập nhật mật khẩu khi phát hiện có sự thay đổi trên trang web. Áp dụng với mọi tài khoản đăng nhập trên thiết bị."
},
"enableUsePasskeys": {
"message": "Ask to save and use passkeys"
"message": "Đưa ra lựa chọn để lưu và sử dụng passkey"
},
"usePasskeysDesc": {
"message": "Ask to save new passkeys or log in with passkeys stored in your vault. Applies to all logged in accounts."
"message": "Đưa ra lựa chọn để lưu passkey mới hoặc đăng nhập bằng passkey đã lưu trong hòm. Áp dụng với mọi tài khoản đăng nhập trên thiết bị."
},
"notificationChangeDesc": {
"message": "Bạn có muốn cập nhật mật khẩu này trên Bitwarden không?"
@ -712,7 +712,7 @@
"message": "Sử dụng một đúp chuột để truy cập vào việc tạo mật khẩu và thông tin đăng nhập phù hợp cho trang web. "
},
"contextMenuItemDescAlt": {
"message": "Use a secondary click to access password generation and matching logins for the website. Applies to all logged in accounts."
"message": "Truy cập trình khởi tạo mật khẩu và các mục đăng nhập đã lưu của trang web bằng cách nhấn đúp chuột. Áp dụng với mọi tài khoản đăng nhập trên thiết bị."
},
"defaultUriMatchDetection": {
"message": "Phương thức kiểm tra URI mặc định",
@ -728,7 +728,7 @@
"message": "Thay đổi màu sắc ứng dụng."
},
"themeDescAlt": {
"message": "Change the application's color theme. Applies to all logged in accounts."
"message": "Thay đổi tông màu giao diện của ứng dụng. Áp dụng với mọi tài khoản đăng nhập trên thiết bị."
},
"dark": {
"message": "Tối",
@ -1061,10 +1061,10 @@
"message": "Tắt cài đặt trình quản lý mật khẩu tích hợp trong trình duyệt của bạn để tránh xung đột."
},
"turnOffBrowserBuiltInPasswordManagerSettingsLink": {
"message": "Edit browser settings."
"message": "Thay đổi cài đặt của trình duyệt."
},
"autofillOverlayVisibilityOff": {
"message": "Off",
"message": "Tắt",
"description": "Overlay setting select option for disabling autofill overlay"
},
"autofillOverlayVisibilityOnFieldFocus": {
@ -1168,7 +1168,7 @@
"message": "Hiển thị một ảnh nhận dạng bên cạnh mỗi lần đăng nhập."
},
"faviconDescAlt": {
"message": "Show a recognizable image next to each login. Applies to all logged in accounts."
"message": "Hiển thị một biểu tượng dễ nhận dạng bên cạnh mỗi mục đăng nhập. Áp dụng với mọi tài khoản đăng nhập trên thiết bị."
},
"enableBadgeCounter": {
"message": "Hiển thị biểu tượng bộ đếm"
@ -1500,7 +1500,7 @@
"message": "Mã PIN không hợp lệ."
},
"tooManyInvalidPinEntryAttemptsLoggingOut": {
"message": "Too many invalid PIN entry attempts. Logging out."
"message": "Mã PIN bị gõ sai quá nhiều lần. Đang đăng xuất."
},
"unlockWithBiometrics": {
"message": "Mở khóa bằng sinh trắc học"

View File

@ -0,0 +1,28 @@
import { KdfConfigService as AbstractKdfConfigService } from "@bitwarden/common/auth/abstractions/kdf-config.service";
import { KdfConfigService } from "@bitwarden/common/auth/services/kdf-config.service";
import {
FactoryOptions,
CachedServices,
factory,
} from "../../../platform/background/service-factories/factory-options";
import {
StateProviderInitOptions,
stateProviderFactory,
} from "../../../platform/background/service-factories/state-provider.factory";
type KdfConfigServiceFactoryOptions = FactoryOptions;
export type KdfConfigServiceInitOptions = KdfConfigServiceFactoryOptions & StateProviderInitOptions;
export function kdfConfigServiceFactory(
cache: { kdfConfigService?: AbstractKdfConfigService } & CachedServices,
opts: KdfConfigServiceInitOptions,
): Promise<AbstractKdfConfigService> {
return factory(
cache,
"kdfConfigService",
opts,
async () => new KdfConfigService(await stateProviderFactory(cache, opts)),
);
}

View File

@ -68,6 +68,7 @@ import {
deviceTrustServiceFactory,
DeviceTrustServiceInitOptions,
} from "./device-trust-service.factory";
import { kdfConfigServiceFactory, KdfConfigServiceInitOptions } from "./kdf-config-service.factory";
import {
keyConnectorServiceFactory,
KeyConnectorServiceInitOptions,
@ -106,7 +107,8 @@ export type LoginStrategyServiceInitOptions = LoginStrategyServiceFactoryOptions
AuthRequestServiceInitOptions &
UserDecryptionOptionsServiceInitOptions &
GlobalStateProviderInitOptions &
BillingAccountProfileStateServiceInitOptions;
BillingAccountProfileStateServiceInitOptions &
KdfConfigServiceInitOptions;
export function loginStrategyServiceFactory(
cache: { loginStrategyService?: LoginStrategyServiceAbstraction } & CachedServices,
@ -140,6 +142,7 @@ export function loginStrategyServiceFactory(
await internalUserDecryptionOptionServiceFactory(cache, opts),
await globalStateProviderFactory(cache, opts),
await billingAccountProfileStateServiceFactory(cache, opts),
await kdfConfigServiceFactory(cache, opts),
),
);
}

View File

@ -22,13 +22,16 @@ import {
stateServiceFactory,
} from "../../../platform/background/service-factories/state-service.factory";
import { KdfConfigServiceInitOptions, kdfConfigServiceFactory } from "./kdf-config-service.factory";
type PinCryptoServiceFactoryOptions = FactoryOptions;
export type PinCryptoServiceInitOptions = PinCryptoServiceFactoryOptions &
StateServiceInitOptions &
CryptoServiceInitOptions &
VaultTimeoutSettingsServiceInitOptions &
LogServiceInitOptions;
LogServiceInitOptions &
KdfConfigServiceInitOptions;
export function pinCryptoServiceFactory(
cache: { pinCryptoService?: PinCryptoServiceAbstraction } & CachedServices,
@ -44,6 +47,7 @@ export function pinCryptoServiceFactory(
await cryptoServiceFactory(cache, opts),
await vaultTimeoutSettingsServiceFactory(cache, opts),
await logServiceFactory(cache, opts),
await kdfConfigServiceFactory(cache, opts),
),
);
}

View File

@ -1,11 +1,13 @@
import { TwoFactorService as AbstractTwoFactorService } from "@bitwarden/common/auth/abstractions/two-factor.service";
import { TwoFactorService } from "@bitwarden/common/auth/services/two-factor.service";
import { GlobalStateProvider } from "@bitwarden/common/platform/state";
import {
FactoryOptions,
CachedServices,
factory,
} from "../../../platform/background/service-factories/factory-options";
import { globalStateProviderFactory } from "../../../platform/background/service-factories/global-state-provider.factory";
import {
I18nServiceInitOptions,
i18nServiceFactory,
@ -19,7 +21,8 @@ type TwoFactorServiceFactoryOptions = FactoryOptions;
export type TwoFactorServiceInitOptions = TwoFactorServiceFactoryOptions &
I18nServiceInitOptions &
PlatformUtilsServiceInitOptions;
PlatformUtilsServiceInitOptions &
GlobalStateProvider;
export async function twoFactorServiceFactory(
cache: { twoFactorService?: AbstractTwoFactorService } & CachedServices,
@ -33,6 +36,7 @@ export async function twoFactorServiceFactory(
new TwoFactorService(
await i18nServiceFactory(cache, opts),
await platformUtilsServiceFactory(cache, opts),
await globalStateProviderFactory(cache, opts),
),
);
service.init();

View File

@ -32,6 +32,7 @@ import {
} from "../../../platform/background/service-factories/state-service.factory";
import { accountServiceFactory, AccountServiceInitOptions } from "./account-service.factory";
import { KdfConfigServiceInitOptions, kdfConfigServiceFactory } from "./kdf-config-service.factory";
import {
internalMasterPasswordServiceFactory,
MasterPasswordServiceInitOptions,
@ -59,7 +60,8 @@ export type UserVerificationServiceInitOptions = UserVerificationServiceFactoryO
PinCryptoServiceInitOptions &
LogServiceInitOptions &
VaultTimeoutSettingsServiceInitOptions &
PlatformUtilsServiceInitOptions;
PlatformUtilsServiceInitOptions &
KdfConfigServiceInitOptions;
export function userVerificationServiceFactory(
cache: { userVerificationService?: AbstractUserVerificationService } & CachedServices,
@ -82,6 +84,7 @@ export function userVerificationServiceFactory(
await logServiceFactory(cache, opts),
await vaultTimeoutSettingsServiceFactory(cache, opts),
await platformUtilsServiceFactory(cache, opts),
await kdfConfigServiceFactory(cache, opts),
),
);
}

View File

@ -12,6 +12,7 @@ import { InternalPolicyService } from "@bitwarden/common/admin-console/abstracti
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
import { AuthService } from "@bitwarden/common/auth/abstractions/auth.service";
import { DeviceTrustServiceAbstraction } from "@bitwarden/common/auth/abstractions/device-trust.service.abstraction";
import { KdfConfigService } from "@bitwarden/common/auth/abstractions/kdf-config.service";
import { InternalMasterPasswordServiceAbstraction } from "@bitwarden/common/auth/abstractions/master-password.service.abstraction";
import { UserVerificationService } from "@bitwarden/common/auth/abstractions/user-verification/user-verification.service.abstraction";
import { AuthenticationStatus } from "@bitwarden/common/auth/enums/authentication-status";
@ -66,6 +67,7 @@ export class LockComponent extends BaseLockComponent {
private routerService: BrowserRouterService,
biometricStateService: BiometricStateService,
accountService: AccountService,
kdfConfigService: KdfConfigService,
) {
super(
masterPasswordService,
@ -90,6 +92,7 @@ export class LockComponent extends BaseLockComponent {
pinCryptoService,
biometricStateService,
accountService,
kdfConfigService,
);
this.successRoute = "/tabs/current";
this.isInitialLockScreen = (window as any).previousPopupUrl == null;

View File

@ -2,7 +2,10 @@ import { Component } from "@angular/core";
import { ActivatedRoute, Router } from "@angular/router";
import { TwoFactorOptionsComponent as BaseTwoFactorOptionsComponent } from "@bitwarden/angular/auth/components/two-factor-options.component";
import { TwoFactorService } from "@bitwarden/common/auth/abstractions/two-factor.service";
import {
TwoFactorProviderDetails,
TwoFactorService,
} from "@bitwarden/common/auth/abstractions/two-factor.service";
import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
@ -27,9 +30,9 @@ export class TwoFactorOptionsComponent extends BaseTwoFactorOptionsComponent {
this.navigateTo2FA();
}
choose(p: any) {
super.choose(p);
this.twoFactorService.setSelectedProvider(p.type);
override async choose(p: TwoFactorProviderDetails) {
await super.choose(p);
await this.twoFactorService.setSelectedProvider(p.type);
this.navigateTo2FA();
}

View File

@ -130,7 +130,9 @@ export default class AutofillService implements AutofillServiceInterface {
if (triggeringOnPageLoad && autoFillOnPageLoadIsEnabled) {
injectedScripts.push("autofiller.js");
} else {
}
if (!triggeringOnPageLoad) {
await this.scriptInjectorService.inject({
tabId: tab.id,
injectDetails: { file: "content/content-message-handler.js", runAt: "document_start" },

View File

@ -3,8 +3,6 @@ import { Subject, firstValueFrom, merge } from "rxjs";
import {
PinCryptoServiceAbstraction,
PinCryptoService,
LoginStrategyServiceAbstraction,
LoginStrategyService,
InternalUserDecryptionOptionsServiceAbstraction,
UserDecryptionOptionsService,
AuthRequestServiceAbstraction,
@ -33,11 +31,11 @@ import { AvatarService as AvatarServiceAbstraction } from "@bitwarden/common/aut
import { DeviceTrustServiceAbstraction } from "@bitwarden/common/auth/abstractions/device-trust.service.abstraction";
import { DevicesServiceAbstraction } from "@bitwarden/common/auth/abstractions/devices/devices.service.abstraction";
import { DevicesApiServiceAbstraction } from "@bitwarden/common/auth/abstractions/devices-api.service.abstraction";
import { KdfConfigService as kdfConfigServiceAbstraction } from "@bitwarden/common/auth/abstractions/kdf-config.service";
import { KeyConnectorService as KeyConnectorServiceAbstraction } from "@bitwarden/common/auth/abstractions/key-connector.service";
import { InternalMasterPasswordServiceAbstraction } from "@bitwarden/common/auth/abstractions/master-password.service.abstraction";
import { SsoLoginServiceAbstraction } from "@bitwarden/common/auth/abstractions/sso-login.service.abstraction";
import { TokenService as TokenServiceAbstraction } from "@bitwarden/common/auth/abstractions/token.service";
import { TwoFactorService as TwoFactorServiceAbstraction } from "@bitwarden/common/auth/abstractions/two-factor.service";
import { UserVerificationApiServiceAbstraction } from "@bitwarden/common/auth/abstractions/user-verification/user-verification-api.service.abstraction";
import { UserVerificationService as UserVerificationServiceAbstraction } from "@bitwarden/common/auth/abstractions/user-verification/user-verification.service.abstraction";
import { AuthenticationStatus } from "@bitwarden/common/auth/enums/authentication-status";
@ -48,11 +46,11 @@ import { AvatarService } from "@bitwarden/common/auth/services/avatar.service";
import { DeviceTrustService } from "@bitwarden/common/auth/services/device-trust.service.implementation";
import { DevicesServiceImplementation } from "@bitwarden/common/auth/services/devices/devices.service.implementation";
import { DevicesApiServiceImplementation } from "@bitwarden/common/auth/services/devices-api.service.implementation";
import { KdfConfigService } from "@bitwarden/common/auth/services/kdf-config.service";
import { KeyConnectorService } from "@bitwarden/common/auth/services/key-connector.service";
import { MasterPasswordService } from "@bitwarden/common/auth/services/master-password/master-password.service";
import { SsoLoginService } from "@bitwarden/common/auth/services/sso-login.service";
import { TokenService } from "@bitwarden/common/auth/services/token.service";
import { TwoFactorService } from "@bitwarden/common/auth/services/two-factor.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 {
@ -275,7 +273,6 @@ export default class MainBackground {
containerService: ContainerService;
auditService: AuditServiceAbstraction;
authService: AuthServiceAbstraction;
loginStrategyService: LoginStrategyServiceAbstraction;
loginEmailService: LoginEmailServiceAbstraction;
importApiService: ImportApiServiceAbstraction;
importService: ImportServiceAbstraction;
@ -299,7 +296,6 @@ export default class MainBackground {
providerService: ProviderServiceAbstraction;
keyConnectorService: KeyConnectorServiceAbstraction;
userVerificationService: UserVerificationServiceAbstraction;
twoFactorService: TwoFactorServiceAbstraction;
vaultFilterService: VaultFilterService;
usernameGenerationService: UsernameGenerationServiceAbstraction;
encryptService: EncryptService;
@ -339,6 +335,7 @@ export default class MainBackground {
intraprocessMessagingSubject: Subject<Message<object>>;
userKeyInitService: UserKeyInitService;
scriptInjectorService: BrowserScriptInjectorService;
kdfConfigService: kdfConfigServiceAbstraction;
onUpdatedRan: boolean;
onReplacedRan: boolean;
@ -493,7 +490,7 @@ export default class MainBackground {
this.accountService,
this.singleUserStateProvider,
);
this.derivedStateProvider = new BackgroundDerivedStateProvider(storageServiceProvider);
this.derivedStateProvider = new BackgroundDerivedStateProvider();
this.stateProvider = new DefaultStateProvider(
this.activeUserStateProvider,
this.singleUserStateProvider,
@ -542,6 +539,9 @@ export default class MainBackground {
this.masterPasswordService = new MasterPasswordService(this.stateProvider);
this.i18nService = new I18nService(BrowserApi.getUILanguage(), this.globalStateProvider);
this.kdfConfigService = new KdfConfigService(this.stateProvider);
this.cryptoService = new BrowserCryptoService(
this.masterPasswordService,
this.keyGenerationService,
@ -553,6 +553,7 @@ export default class MainBackground {
this.accountService,
this.stateProvider,
this.biometricStateService,
this.kdfConfigService,
);
this.appIdService = new AppIdService(this.globalStateProvider);
@ -607,8 +608,6 @@ export default class MainBackground {
this.stateService,
);
this.twoFactorService = new TwoFactorService(this.i18nService, this.platformUtilsService);
this.userDecryptionOptionsService = new UserDecryptionOptionsService(this.stateProvider);
this.devicesApiService = new DevicesApiServiceImplementation(this.apiService);
@ -652,31 +651,6 @@ export default class MainBackground {
this.loginEmailService = new LoginEmailService(this.stateProvider);
this.loginStrategyService = new LoginStrategyService(
this.accountService,
this.masterPasswordService,
this.cryptoService,
this.apiService,
this.tokenService,
this.appIdService,
this.platformUtilsService,
this.messagingService,
this.logService,
this.keyConnectorService,
this.environmentService,
this.stateService,
this.twoFactorService,
this.i18nService,
this.encryptService,
this.passwordStrengthService,
this.policyService,
this.deviceTrustService,
this.authRequestService,
this.userDecryptionOptionsService,
this.globalStateProvider,
this.billingAccountProfileStateService,
);
this.ssoLoginService = new SsoLoginService(this.stateProvider);
this.userVerificationApiService = new UserVerificationApiService(this.apiService);
@ -725,6 +699,7 @@ export default class MainBackground {
this.cryptoService,
this.vaultTimeoutSettingsService,
this.logService,
this.kdfConfigService,
);
this.userVerificationService = new UserVerificationService(
@ -739,6 +714,7 @@ export default class MainBackground {
this.logService,
this.vaultTimeoutSettingsService,
this.platformUtilsService,
this.kdfConfigService,
);
this.vaultFilterService = new VaultFilterService(
@ -861,7 +837,7 @@ export default class MainBackground {
this.cipherService,
this.cryptoService,
this.cryptoFunctionService,
this.stateService,
this.kdfConfigService,
);
this.organizationVaultExportService = new OrganizationVaultExportService(
@ -869,8 +845,8 @@ export default class MainBackground {
this.apiService,
this.cryptoService,
this.cryptoFunctionService,
this.stateService,
this.collectionService,
this.kdfConfigService,
);
this.exportService = new VaultExportService(
@ -1104,8 +1080,7 @@ export default class MainBackground {
this.userKeyInitService.listenForActiveUserChangesToSetUserKey();
await (this.i18nService as I18nService).init();
await (this.eventUploadService as EventUploadService).init(true);
this.twoFactorService.init();
(this.eventUploadService as EventUploadService).init(true);
if (this.popupOnlyContext) {
return;

View File

@ -4,6 +4,10 @@ import {
AccountServiceInitOptions,
accountServiceFactory,
} from "../../../auth/background/service-factories/account-service.factory";
import {
KdfConfigServiceInitOptions,
kdfConfigServiceFactory,
} from "../../../auth/background/service-factories/kdf-config-service.factory";
import {
internalMasterPasswordServiceFactory,
MasterPasswordServiceInitOptions,
@ -18,7 +22,10 @@ import {
} from "../../background/service-factories/log-service.factory";
import { BrowserCryptoService } from "../../services/browser-crypto.service";
import { biometricStateServiceFactory } from "./biometric-state-service.factory";
import {
BiometricStateServiceInitOptions,
biometricStateServiceFactory,
} from "./biometric-state-service.factory";
import {
cryptoFunctionServiceFactory,
CryptoFunctionServiceInitOptions,
@ -46,7 +53,9 @@ export type CryptoServiceInitOptions = CryptoServiceFactoryOptions &
LogServiceInitOptions &
StateServiceInitOptions &
AccountServiceInitOptions &
StateProviderInitOptions;
StateProviderInitOptions &
BiometricStateServiceInitOptions &
KdfConfigServiceInitOptions;
export function cryptoServiceFactory(
cache: { cryptoService?: AbstractCryptoService } & CachedServices,
@ -68,6 +77,7 @@ export function cryptoServiceFactory(
await accountServiceFactory(cache, opts),
await stateProviderFactory(cache, opts),
await biometricStateServiceFactory(cache, opts),
await kdfConfigServiceFactory(cache, opts),
),
);
}

View File

@ -3,15 +3,10 @@ import { DerivedStateProvider } from "@bitwarden/common/platform/state";
import { BackgroundDerivedStateProvider } from "../../state/background-derived-state.provider";
import { CachedServices, FactoryOptions, factory } from "./factory-options";
import {
StorageServiceProviderInitOptions,
storageServiceProviderFactory,
} from "./storage-service-provider.factory";
type DerivedStateProviderFactoryOptions = FactoryOptions;
export type DerivedStateProviderInitOptions = DerivedStateProviderFactoryOptions &
StorageServiceProviderInitOptions;
export type DerivedStateProviderInitOptions = DerivedStateProviderFactoryOptions;
export async function derivedStateProviderFactory(
cache: { derivedStateProvider?: DerivedStateProvider } & CachedServices,
@ -21,7 +16,6 @@ export async function derivedStateProviderFactory(
cache,
"derivedStateProvider",
opts,
async () =>
new BackgroundDerivedStateProvider(await storageServiceProviderFactory(cache, opts)),
async () => new BackgroundDerivedStateProvider(),
);
}

View File

@ -238,10 +238,6 @@ export class BrowserApi {
return typeof window !== "undefined" && window === BrowserApi.getBackgroundPage();
}
static getApplicationVersion(): string {
return chrome.runtime.getManifest().version;
}
/**
* Gets the extension views that match the given properties. This method is not
* available within background service worker. As a result, it will return an

View File

@ -0,0 +1,9 @@
<footer
class="tw-p-3 tw-border-0 tw-border-solid tw-border-t tw-border-secondary-300 tw-bg-background"
>
<div class="tw-max-w-screen-sm tw-mx-auto">
<div class="tw-flex tw-justify-start tw-gap-2">
<ng-content></ng-content>
</div>
</div>
</footer>

View File

@ -0,0 +1,9 @@
import { Component } from "@angular/core";
@Component({
selector: "popup-footer",
templateUrl: "popup-footer.component.html",
standalone: true,
imports: [],
})
export class PopupFooterComponent {}

View File

@ -0,0 +1,19 @@
<header
class="tw-p-4 tw-border-0 tw-border-solid tw-border-b tw-border-secondary-300 tw-bg-background"
>
<div class="tw-max-w-screen-sm tw-mx-auto tw-flex tw-justify-between tw-w-full">
<div class="tw-inline-flex tw-items-center tw-gap-2 tw-h-9">
<button
bitIconButton="bwi-back"
type="button"
*ngIf="showBackButton"
[title]="'back' | i18n"
[ariaLabel]="'back' | i18n"
></button>
<h1 bitTypography="h3" class="!tw-mb-0.5 tw-text-headers">{{ pageTitle }}</h1>
</div>
<div class="tw-inline-flex tw-items-center tw-gap-2 tw-h-9">
<ng-content select="[slot=end]"></ng-content>
</div>
</div>
</header>

View File

@ -0,0 +1,34 @@
import { BooleanInput, coerceBooleanProperty } from "@angular/cdk/coercion";
import { CommonModule, Location } from "@angular/common";
import { Component, Input } from "@angular/core";
import { JslibModule } from "@bitwarden/angular/jslib.module";
import { IconButtonModule, TypographyModule } from "@bitwarden/components";
@Component({
selector: "popup-header",
templateUrl: "popup-header.component.html",
standalone: true,
imports: [TypographyModule, CommonModule, IconButtonModule, JslibModule],
})
export class PopupHeaderComponent {
/** Display the back button, which uses Location.back() to go back one page in history */
@Input()
get showBackButton() {
return this._showBackButton;
}
set showBackButton(value: BooleanInput) {
this._showBackButton = coerceBooleanProperty(value);
}
private _showBackButton = false;
/** Title string that will be inserted as an h1 */
@Input({ required: true }) pageTitle: string;
constructor(private location: Location) {}
back() {
this.location.back();
}
}

View File

@ -0,0 +1,138 @@
import { Meta, Story, Canvas } from "@storybook/addon-docs";
import * as stories from "./popup-layout.stories";
<Meta of={stories} />
Please note that because these stories use `router-outlet`, there are issues with rendering content
when Light & Dark mode is selected. The stories are best viewed by selecting one color mode.
# Popup Tab Navigation
The popup tab navigation component composes together the popup page and the bottom tab navigation
footer. This component is intended to be used a level _above_ each extension tab's page code.
The navigation footer contains the 4 main page links for the browser extension. It uses the Angular
router to determine which page is currently active, and style the button appropriately. Clicking on
the buttons will navigate to the correct route. The navigation footer has a max-width built in so
that the page looks nice when the extension is popped out.
Long button names will be ellipsed.
Usage example:
```html
<popup-tab-navigation>
<popup-page></popup-page>
</popup-tab-navigation>
```
# Popup Page
The popup page handles positioning a page's `header` and `footer` elements, and inserting the rest
of the content into the `main` element with scroll. There is also a max-width built in so that the
page looks nice when the extension is popped out.
**Slots**
- `header`
- Use `popup-header` component.
- Every page should have a header.
- `footer`
- Use the `popup-footer` component.
- Not every page will have a footer.
- default
- Whatever content you want in `main`.
Basic usage example:
```html
<popup-page>
<popup-header slot="header"></popup-header>
<div>This is content</div>
<popup-footer slot="footer"></popup-footer>
</popup-page>
```
## Popup header
**Args**
- `pageTitle`: required
- Inserts title as an `h1`.
- `showBackButton`: optional, defaults to `false`
- Toggles the back button to appear. The back button uses `Location.back()` to navigate back one
page in history.
**Slots**
- `end`
- Use to insert one or more interactive elements.
- The header handles the spacing between elements passed to the `end` slot.
Usage example:
```html
<popup-header pageTitle="Test" showBackButton>
<ng-container slot="end">
<button></button>
<button></button>
</ng-container>
</popup-header>
```
Common interactive elements to insert into the `end` slot are:
- `app-current-account`: shows current account and switcher
- `app-pop-out`: shows popout button when the extension is not already popped out
- "Add" button: this can be accomplished with the Button component and any custom functionality for
that particular page
## Popup footer
Popup footer should be used when the page displays action buttons. It functions similarly to the
Dialog footer in that the calling code is responsible for passing in the different buttons that need
to be rendered.
Usage example:
```html
<popup-footer>
<button bitButton buttonType="primary">Save</button>
<button bitButton buttonType="secondary">Cancel</button>
</popup-footer>
```
# Page types
There are a few types of pages that are used in the browser extension.
View the story source code to see examples of how to construct these types of pages.
## Extension Tab
Example of wrapping an extension page in the `popup-tab-navigation` component.
<Canvas>
<Story of={stories.PopupTabNavigation} />
</Canvas>
## Extension Page
Examples of using just the `popup-page` component, without and with a footer.
<Canvas>
<Story of={stories.PopupPage} />
</Canvas>
<Canvas>
<Story of={stories.PopupPageWithFooter} />
</Canvas>
## Popped out
When the browser extension is popped out, the "popout" button should not be passed to the header.
<Canvas>
<Story of={stories.PoppedOut} />
</Canvas>

View File

@ -0,0 +1,367 @@
import { CommonModule } from "@angular/common";
import { Component, importProvidersFrom } from "@angular/core";
import { RouterModule } from "@angular/router";
import { Meta, StoryObj, applicationConfig, moduleMetadata } from "@storybook/angular";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import {
AvatarModule,
ButtonModule,
I18nMockService,
IconButtonModule,
} from "@bitwarden/components";
import { PopupFooterComponent } from "./popup-footer.component";
import { PopupHeaderComponent } from "./popup-header.component";
import { PopupPageComponent } from "./popup-page.component";
import { PopupTabNavigationComponent } from "./popup-tab-navigation.component";
@Component({
selector: "extension-container",
template: `
<div class="tw-h-[640px] tw-w-[380px] tw-border tw-border-solid tw-border-secondary-300">
<ng-content></ng-content>
</div>
`,
standalone: true,
})
class ExtensionContainerComponent {}
@Component({
selector: "vault-placeholder",
template: `
<div class="tw-mb-8 tw-text-main">vault item</div>
<div class="tw-my-8 tw-text-main">vault item</div>
<div class="tw-my-8 tw-text-main">vault item</div>
<div class="tw-my-8 tw-text-main">vault item</div>
<div class="tw-my-8 tw-text-main">vault item</div>
<div class="tw-my-8 tw-text-main">vault item</div>
<div class="tw-my-8 tw-text-main">vault item</div>
<div class="tw-my-8 tw-text-main">vault item</div>
<div class="tw-my-8 tw-text-main">vault item</div>
<div class="tw-my-8 tw-text-main">vault item</div>
<div class="tw-my-8 tw-text-main">vault item</div>
<div class="tw-my-8 tw-text-main">vault item</div>
<div class="tw-my-8 tw-text-main">vault item last item</div>
`,
standalone: true,
})
class VaultComponent {}
@Component({
selector: "generator-placeholder",
template: ` <div class="tw-text-main">generator stuff here</div> `,
standalone: true,
})
class GeneratorComponent {}
@Component({
selector: "send-placeholder",
template: ` <div class="tw-text-main">send some stuff</div> `,
standalone: true,
})
class SendComponent {}
@Component({
selector: "settings-placeholder",
template: ` <div class="tw-text-main">change your settings</div> `,
standalone: true,
})
class SettingsComponent {}
@Component({
selector: "mock-add-button",
template: `
<button bitButton buttonType="primary" type="button">
<i class="bwi bwi-plus-f" aria-hidden="true"></i>
Add
</button>
`,
standalone: true,
imports: [ButtonModule],
})
class MockAddButtonComponent {}
@Component({
selector: "mock-popout-button",
template: `
<button
bitIconButton="bwi-popout"
size="small"
type="button"
title="Pop out"
aria-label="Pop out"
></button>
`,
standalone: true,
imports: [IconButtonModule],
})
class MockPopoutButtonComponent {}
@Component({
selector: "mock-current-account",
template: `
<button class="tw-bg-transparent tw-border-none" type="button">
<bit-avatar text="Ash Ketchum" size="small"></bit-avatar>
</button>
`,
standalone: true,
imports: [AvatarModule],
})
class MockCurrentAccountComponent {}
@Component({
selector: "mock-vault-page",
template: `
<popup-page>
<popup-header slot="header" pageTitle="Test">
<ng-container slot="end">
<mock-add-button></mock-add-button>
<mock-popout-button></mock-popout-button>
<mock-current-account></mock-current-account>
</ng-container>
</popup-header>
<vault-placeholder></vault-placeholder>
</popup-page>
`,
standalone: true,
imports: [
PopupPageComponent,
PopupHeaderComponent,
MockAddButtonComponent,
MockPopoutButtonComponent,
MockCurrentAccountComponent,
VaultComponent,
],
})
class MockVaultPageComponent {}
@Component({
selector: "mock-vault-page-popped",
template: `
<popup-page>
<popup-header slot="header" pageTitle="Test">
<ng-container slot="end">
<mock-add-button></mock-add-button>
<mock-current-account></mock-current-account>
</ng-container>
</popup-header>
<vault-placeholder></vault-placeholder>
</popup-page>
`,
standalone: true,
imports: [
PopupPageComponent,
PopupHeaderComponent,
MockAddButtonComponent,
MockPopoutButtonComponent,
MockCurrentAccountComponent,
VaultComponent,
],
})
class MockVaultPagePoppedComponent {}
@Component({
selector: "mock-generator-page",
template: `
<popup-page>
<popup-header slot="header" pageTitle="Test">
<ng-container slot="end">
<mock-add-button></mock-add-button>
<mock-popout-button></mock-popout-button>
<mock-current-account></mock-current-account>
</ng-container>
</popup-header>
<generator-placeholder></generator-placeholder>
</popup-page>
`,
standalone: true,
imports: [
PopupPageComponent,
PopupHeaderComponent,
MockAddButtonComponent,
MockPopoutButtonComponent,
MockCurrentAccountComponent,
GeneratorComponent,
],
})
class MockGeneratorPageComponent {}
@Component({
selector: "mock-send-page",
template: `
<popup-page>
<popup-header slot="header" pageTitle="Test">
<ng-container slot="end">
<mock-add-button></mock-add-button>
<mock-popout-button></mock-popout-button>
<mock-current-account></mock-current-account>
</ng-container>
</popup-header>
<send-placeholder></send-placeholder>
</popup-page>
`,
standalone: true,
imports: [
PopupPageComponent,
PopupHeaderComponent,
MockAddButtonComponent,
MockPopoutButtonComponent,
MockCurrentAccountComponent,
SendComponent,
],
})
class MockSendPageComponent {}
@Component({
selector: "mock-settings-page",
template: `
<popup-page>
<popup-header slot="header" pageTitle="Test">
<ng-container slot="end">
<mock-add-button></mock-add-button>
<mock-popout-button></mock-popout-button>
<mock-current-account></mock-current-account>
</ng-container>
</popup-header>
<settings-placeholder></settings-placeholder>
</popup-page>
`,
standalone: true,
imports: [
PopupPageComponent,
PopupHeaderComponent,
MockAddButtonComponent,
MockPopoutButtonComponent,
MockCurrentAccountComponent,
SettingsComponent,
],
})
class MockSettingsPageComponent {}
@Component({
selector: "mock-vault-subpage",
template: `
<popup-page>
<popup-header slot="header" pageTitle="Test" showBackButton>
<ng-container slot="end">
<mock-popout-button></mock-popout-button>
</ng-container>
</popup-header>
<vault-placeholder></vault-placeholder>
<popup-footer slot="footer">
<button bitButton buttonType="primary">Save</button>
<button bitButton buttonType="secondary">Cancel</button>
</popup-footer>
</popup-page>
`,
standalone: true,
imports: [
PopupPageComponent,
PopupHeaderComponent,
PopupFooterComponent,
ButtonModule,
MockAddButtonComponent,
MockPopoutButtonComponent,
MockCurrentAccountComponent,
VaultComponent,
],
})
class MockVaultSubpageComponent {}
export default {
title: "Browser/Popup Layout",
component: PopupPageComponent,
decorators: [
moduleMetadata({
imports: [
PopupTabNavigationComponent,
CommonModule,
RouterModule,
ExtensionContainerComponent,
MockVaultSubpageComponent,
MockVaultPageComponent,
MockSendPageComponent,
MockGeneratorPageComponent,
MockSettingsPageComponent,
MockVaultPagePoppedComponent,
],
providers: [
{
provide: I18nService,
useFactory: () => {
return new I18nMockService({
back: "Back",
});
},
},
],
}),
applicationConfig({
providers: [
importProvidersFrom(
RouterModule.forRoot(
[
{ path: "", redirectTo: "vault", pathMatch: "full" },
{ path: "vault", component: MockVaultPageComponent },
{ path: "generator", component: MockGeneratorPageComponent },
{ path: "send", component: MockSendPageComponent },
{ path: "settings", component: MockSettingsPageComponent },
// in case you are coming from a story that also uses the router
{ path: "**", redirectTo: "vault" },
],
{ useHash: true },
),
),
],
}),
],
} as Meta;
type Story = StoryObj<PopupPageComponent>;
export const PopupTabNavigation: Story = {
render: (args) => ({
props: args,
template: /* HTML */ `
<extension-container>
<popup-tab-navigation>
<router-outlet></router-outlet>
</popup-tab-navigation>
</extension-container>
`,
}),
};
export const PopupPage: Story = {
render: (args) => ({
props: args,
template: /* HTML */ `
<extension-container>
<mock-vault-page></mock-vault-page>
</extension-container>
`,
}),
};
export const PopupPageWithFooter: Story = {
render: (args) => ({
props: args,
template: /* HTML */ `
<extension-container>
<mock-vault-subpage></mock-vault-subpage>
</extension-container>
`,
}),
};
export const PoppedOut: Story = {
render: (args) => ({
props: args,
template: /* HTML */ `
<div class="tw-h-[640px] tw-w-[900px] tw-border tw-border-solid tw-border-secondary-300">
<mock-vault-page-popped></mock-vault-page-popped>
</div>
`,
}),
};

View File

@ -0,0 +1,7 @@
<ng-content select="[slot=header]"></ng-content>
<main class="tw-bg-background-alt tw-p-3 tw-flex-1 tw-overflow-y-auto">
<div class="tw-max-w-screen-sm tw-mx-auto">
<ng-content></ng-content>
</div>
</main>
<ng-content select="[slot=footer]"></ng-content>

View File

@ -0,0 +1,11 @@
import { Component } from "@angular/core";
@Component({
selector: "popup-page",
templateUrl: "popup-page.component.html",
standalone: true,
host: {
class: "tw-h-full tw-flex tw-flex-col tw-flex-1 tw-overflow-y-auto",
},
})
export class PopupPageComponent {}

View File

@ -0,0 +1,32 @@
<div class="tw-h-full tw-overflow-y-auto [&>*]:tw-h-full [&>*]:tw-overflow-y-auto">
<ng-content></ng-content>
</div>
<footer class="tw-bg-background tw-border-0 tw-border-t tw-border-secondary-300 tw-border-solid">
<div class="tw-max-w-screen-sm tw-mx-auto">
<div class="tw-flex tw-flex-1">
<a
*ngFor="let button of navButtons"
class="tw-group tw-flex tw-flex-col tw-items-center tw-gap-1 tw-px-0.5 tw-pb-2 tw-pt-3 tw-w-1/4 tw-no-underline hover:tw-no-underline hover:tw-text-primary-600 hover:tw-bg-primary-100 tw-border-2 tw-border-solid tw-border-transparent focus-visible:tw-rounded-lg focus-visible:tw-border-primary-500"
[ngClass]="rla.isActive ? 'tw-font-bold tw-text-primary-600' : 'tw-text-muted'"
[title]="button.label"
[routerLink]="button.page"
routerLinkActive
#rla="routerLinkActive"
ariaCurrentWhenActive="page"
>
<i *ngIf="!rla.isActive" class="bwi bwi-lg bwi-{{ button.iconKey }}" aria-hidden="true"></i>
<i
*ngIf="rla.isActive"
class="bwi bwi-lg bwi-{{ button.iconKeyActive }}"
aria-hidden="true"
></i>
<span
class="tw-truncate tw-max-w-full"
[ngClass]="!rla.isActive && 'group-hover:tw-underline'"
>
{{ button.label }}
</span>
</a>
</div>
</div>
</footer>

View File

@ -0,0 +1,43 @@
import { CommonModule } from "@angular/common";
import { Component } from "@angular/core";
import { RouterModule } from "@angular/router";
import { LinkModule } from "@bitwarden/components";
@Component({
selector: "popup-tab-navigation",
templateUrl: "popup-tab-navigation.component.html",
standalone: true,
imports: [CommonModule, LinkModule, RouterModule],
host: {
class: "tw-block tw-h-full tw-w-full tw-flex tw-flex-col",
},
})
export class PopupTabNavigationComponent {
navButtons = [
{
label: "Vault",
page: "/vault",
iconKey: "lock",
iconKeyActive: "lock-f",
},
{
label: "Generator",
page: "/generator",
iconKey: "generate",
iconKeyActive: "generate-f",
},
{
label: "Send",
page: "/send",
iconKey: "send",
iconKeyActive: "send-f",
},
{
label: "Settings",
page: "/settings",
iconKey: "cog",
iconKeyActive: "cog-f",
},
];
}

View File

@ -78,6 +78,11 @@ export default abstract class AbstractChromeStorageService
async save(key: string, obj: any): Promise<void> {
obj = objToStore(obj);
if (obj == null) {
// Safari does not support set of null values
return this.remove(key);
}
const keyedObj = { [key]: obj };
return new Promise<void>((resolve) => {
this.chromeStorageApi.set(keyedObj, () => {

View File

@ -62,6 +62,17 @@ describe("ChromeStorageApiService", () => {
expect.any(Function),
);
});
it("removes the key when the value is null", async () => {
const removeMock = chrome.storage.local.remove as jest.Mock;
removeMock.mockImplementation((key, callback) => {
delete store[key];
callback();
});
const key = "key";
await service.save(key, null);
expect(removeMock).toHaveBeenCalledWith(key, expect.any(Function));
});
});
describe("get", () => {

View File

@ -1,6 +1,7 @@
import { firstValueFrom } from "rxjs";
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
import { KdfConfigService } from "@bitwarden/common/auth/abstractions/kdf-config.service";
import { InternalMasterPasswordServiceAbstraction } from "@bitwarden/common/auth/abstractions/master-password.service.abstraction";
import { CryptoFunctionService } from "@bitwarden/common/platform/abstractions/crypto-function.service";
import { EncryptService } from "@bitwarden/common/platform/abstractions/encrypt.service";
@ -28,6 +29,7 @@ export class BrowserCryptoService extends CryptoService {
accountService: AccountService,
stateProvider: StateProvider,
private biometricStateService: BiometricStateService,
kdfConfigService: KdfConfigService,
) {
super(
masterPasswordService,
@ -39,6 +41,7 @@ export class BrowserCryptoService extends CryptoService {
stateService,
accountService,
stateProvider,
kdfConfigService,
);
}
override async hasUserKeyStored(keySuffix: KeySuffixOptions, userId?: UserId): Promise<boolean> {

View File

@ -175,11 +175,13 @@ export abstract class BrowserPlatformUtilsService implements PlatformUtilsServic
}
getApplicationVersion(): Promise<string> {
return Promise.resolve(BrowserApi.getApplicationVersion());
const manifest = chrome.runtime.getManifest();
return Promise.resolve(manifest.version_name ?? manifest.version);
}
async getApplicationVersionNumber(): Promise<string> {
return (await this.getApplicationVersion()).split(RegExp("[+|-]"))[0].trim();
getApplicationVersionNumber(): Promise<string> {
const manifest = chrome.runtime.getManifest();
return Promise.resolve(manifest.version.split(RegExp("[+|-]"))[0].trim());
}
supportsWebAuthn(win: Window): boolean {

View File

@ -1,9 +1,5 @@
import { Observable } from "rxjs";
import {
AbstractStorageService,
ObservableStorageService,
} from "@bitwarden/common/platform/abstractions/storage.service";
import { DeriveDefinition, DerivedState } from "@bitwarden/common/platform/state";
// eslint-disable-next-line import/no-restricted-paths -- extending this class for this client
import { DefaultDerivedStateProvider } from "@bitwarden/common/platform/state/implementations/default-derived-state.provider";
@ -16,14 +12,11 @@ export class BackgroundDerivedStateProvider extends DefaultDerivedStateProvider
parentState$: Observable<TFrom>,
deriveDefinition: DeriveDefinition<TFrom, TTo, TDeps>,
dependencies: TDeps,
storageLocation: [string, AbstractStorageService & ObservableStorageService],
): DerivedState<TTo> {
const [cacheKey, storageService] = storageLocation;
return new BackgroundDerivedState(
parentState$,
deriveDefinition,
storageService,
cacheKey,
deriveDefinition.buildCacheKey(),
dependencies,
);
}

View File

@ -1,10 +1,7 @@
import { Observable, Subscription } from "rxjs";
import { Observable, Subscription, concatMap } from "rxjs";
import { Jsonify } from "type-fest";
import {
AbstractStorageService,
ObservableStorageService,
} from "@bitwarden/common/platform/abstractions/storage.service";
import { Utils } from "@bitwarden/common/platform/misc/utils";
import { DeriveDefinition } from "@bitwarden/common/platform/state";
// eslint-disable-next-line import/no-restricted-paths -- extending this class for this client
import { DefaultDerivedState } from "@bitwarden/common/platform/state/implementations/default-derived-state";
@ -22,11 +19,10 @@ export class BackgroundDerivedState<
constructor(
parentState$: Observable<TFrom>,
deriveDefinition: DeriveDefinition<TFrom, TTo, TDeps>,
memoryStorage: AbstractStorageService & ObservableStorageService,
portName: string,
dependencies: TDeps,
) {
super(parentState$, deriveDefinition, memoryStorage, dependencies);
super(parentState$, deriveDefinition, dependencies);
// listen for foreground derived states to connect
BrowserApi.addListener(chrome.runtime.onConnect, (port) => {
@ -42,7 +38,20 @@ export class BackgroundDerivedState<
});
port.onMessage.addListener(listenerCallback);
const stateSubscription = this.state$.subscribe();
const stateSubscription = this.state$
.pipe(
concatMap(async (state) => {
await this.sendMessage(
{
action: "nextState",
data: JSON.stringify(state),
id: Utils.newGuid(),
},
port,
);
}),
)
.subscribe();
this.portSubscriptions.set(port, stateSubscription);
});

View File

@ -4,14 +4,13 @@
*/
import { NgZone } from "@angular/core";
import { FakeStorageService } from "@bitwarden/common/../spec/fake-storage.service";
import { awaitAsync, trackEmissions } from "@bitwarden/common/../spec/utils";
import { mock } from "jest-mock-extended";
import { Subject, firstValueFrom } from "rxjs";
import { DeriveDefinition } from "@bitwarden/common/platform/state";
// eslint-disable-next-line import/no-restricted-paths -- needed to define a derive definition
import { StateDefinition } from "@bitwarden/common/platform/state/state-definition";
import { awaitAsync, trackEmissions, ObservableTracker } from "@bitwarden/common/spec";
import { mockPorts } from "../../../spec/mock-port.spec-util";
@ -22,6 +21,7 @@ const stateDefinition = new StateDefinition("test", "memory");
const deriveDefinition = new DeriveDefinition(stateDefinition, "test", {
derive: (dateString: string) => (dateString == null ? null : new Date(dateString)),
deserializer: (dateString: string) => (dateString == null ? null : new Date(dateString)),
cleanupDelayMs: 1000,
});
// Mock out the runInsideAngular operator so we don't have to deal with zone.js
@ -35,7 +35,6 @@ describe("foreground background derived state interactions", () => {
let foreground: ForegroundDerivedState<Date>;
let background: BackgroundDerivedState<string, Date, Record<string, unknown>>;
let parentState$: Subject<string>;
let memoryStorage: FakeStorageService;
const initialParent = "2020-01-01";
const ngZone = mock<NgZone>();
const portName = "testPort";
@ -43,16 +42,9 @@ describe("foreground background derived state interactions", () => {
beforeEach(() => {
mockPorts();
parentState$ = new Subject<string>();
memoryStorage = new FakeStorageService();
background = new BackgroundDerivedState(
parentState$,
deriveDefinition,
memoryStorage,
portName,
{},
);
foreground = new ForegroundDerivedState(deriveDefinition, memoryStorage, portName, ngZone);
background = new BackgroundDerivedState(parentState$, deriveDefinition, portName, {});
foreground = new ForegroundDerivedState(deriveDefinition, portName, ngZone);
});
afterEach(() => {
@ -72,21 +64,13 @@ describe("foreground background derived state interactions", () => {
});
it("should initialize a late-connected foreground", async () => {
const newForeground = new ForegroundDerivedState(
deriveDefinition,
memoryStorage,
portName,
ngZone,
);
const backgroundEmissions = trackEmissions(background.state$);
const newForeground = new ForegroundDerivedState(deriveDefinition, portName, ngZone);
const backgroundTracker = new ObservableTracker(background.state$);
parentState$.next(initialParent);
await awaitAsync();
const foregroundTracker = new ObservableTracker(newForeground.state$);
const foregroundEmissions = trackEmissions(newForeground.state$);
await awaitAsync(10);
expect(backgroundEmissions).toEqual([new Date(initialParent)]);
expect(foregroundEmissions).toEqual([new Date(initialParent)]);
expect(await backgroundTracker.expectEmission()).toEqual(new Date(initialParent));
expect(await foregroundTracker.expectEmission()).toEqual(new Date(initialParent));
});
describe("forceValue", () => {

View File

@ -1,11 +1,6 @@
import { NgZone } from "@angular/core";
import { Observable } from "rxjs";
import {
AbstractStorageService,
ObservableStorageService,
} from "@bitwarden/common/platform/abstractions/storage.service";
import { StorageServiceProvider } from "@bitwarden/common/platform/services/storage-service.provider";
import { DeriveDefinition, DerivedState } from "@bitwarden/common/platform/state";
// eslint-disable-next-line import/no-restricted-paths -- extending this class for this client
import { DefaultDerivedStateProvider } from "@bitwarden/common/platform/state/implementations/default-derived-state.provider";
@ -14,19 +9,18 @@ import { DerivedStateDependencies } from "@bitwarden/common/src/types/state";
import { ForegroundDerivedState } from "./foreground-derived-state";
export class ForegroundDerivedStateProvider extends DefaultDerivedStateProvider {
constructor(
storageServiceProvider: StorageServiceProvider,
private ngZone: NgZone,
) {
super(storageServiceProvider);
constructor(private ngZone: NgZone) {
super();
}
override buildDerivedState<TFrom, TTo, TDeps extends DerivedStateDependencies>(
_parentState$: Observable<TFrom>,
deriveDefinition: DeriveDefinition<TFrom, TTo, TDeps>,
_dependencies: TDeps,
storageLocation: [string, AbstractStorageService & ObservableStorageService],
): DerivedState<TTo> {
const [cacheKey, storageService] = storageLocation;
return new ForegroundDerivedState(deriveDefinition, storageService, cacheKey, this.ngZone);
return new ForegroundDerivedState(
deriveDefinition,
deriveDefinition.buildCacheKey(),
this.ngZone,
);
}
}

View File

@ -1,11 +1,5 @@
/**
* need to update test environment so structuredClone works appropriately
* @jest-environment ../../libs/shared/test.environment.ts
*/
import { NgZone } from "@angular/core";
import { awaitAsync, trackEmissions } from "@bitwarden/common/../spec";
import { FakeStorageService } from "@bitwarden/common/../spec/fake-storage.service";
import { awaitAsync } from "@bitwarden/common/../spec";
import { mock } from "jest-mock-extended";
import { DeriveDefinition } from "@bitwarden/common/platform/state";
@ -32,15 +26,12 @@ jest.mock("../browser/run-inside-angular.operator", () => {
describe("ForegroundDerivedState", () => {
let sut: ForegroundDerivedState<Date>;
let memoryStorage: FakeStorageService;
const portName = "testPort";
const ngZone = mock<NgZone>();
beforeEach(() => {
memoryStorage = new FakeStorageService();
memoryStorage.internalUpdateValuesRequireDeserialization(true);
mockPorts();
sut = new ForegroundDerivedState(deriveDefinition, memoryStorage, portName, ngZone);
sut = new ForegroundDerivedState(deriveDefinition, portName, ngZone);
});
afterEach(() => {
@ -67,18 +58,4 @@ describe("ForegroundDerivedState", () => {
expect(disconnectSpy).toHaveBeenCalled();
expect(sut["port"]).toBeNull();
});
it("should emit when the memory storage updates", async () => {
const dateString = "2020-01-01";
const emissions = trackEmissions(sut.state$);
await memoryStorage.save(deriveDefinition.storageKey, {
derived: true,
value: new Date(dateString),
});
await awaitAsync();
expect(emissions).toEqual([new Date(dateString)]);
});
});

View File

@ -6,19 +6,14 @@ import {
filter,
firstValueFrom,
map,
merge,
of,
share,
switchMap,
tap,
timer,
} from "rxjs";
import { Jsonify, JsonObject } from "type-fest";
import { Jsonify } from "type-fest";
import {
AbstractStorageService,
ObservableStorageService,
} from "@bitwarden/common/platform/abstractions/storage.service";
import { Utils } from "@bitwarden/common/platform/misc/utils";
import { DeriveDefinition, DerivedState } from "@bitwarden/common/platform/state";
import { DerivedStateDependencies } from "@bitwarden/common/types/state";
@ -27,41 +22,28 @@ import { fromChromeEvent } from "../browser/from-chrome-event";
import { runInsideAngular } from "../browser/run-inside-angular.operator";
export class ForegroundDerivedState<TTo> implements DerivedState<TTo> {
private storageKey: string;
private port: chrome.runtime.Port;
private backgroundResponses$: Observable<DerivedStateMessage>;
state$: Observable<TTo>;
constructor(
private deriveDefinition: DeriveDefinition<unknown, TTo, DerivedStateDependencies>,
private memoryStorage: AbstractStorageService & ObservableStorageService,
private portName: string,
private ngZone: NgZone,
) {
this.storageKey = deriveDefinition.storageKey;
const initialStorageGet$ = defer(() => {
return this.getStoredValue();
}).pipe(
filter((s) => s.derived),
map((s) => s.value),
);
const latestStorage$ = this.memoryStorage.updates$.pipe(
filter((s) => s.key === this.storageKey),
switchMap(async (storageUpdate) => {
if (storageUpdate.updateType === "remove") {
return null;
}
return await this.getStoredValue();
}),
filter((s) => s.derived),
map((s) => s.value),
);
const latestValueFromPort$ = (port: chrome.runtime.Port) => {
return fromChromeEvent(port.onMessage).pipe(
map(([message]) => message as DerivedStateMessage),
filter((message) => message.originator === "background" && message.action === "nextState"),
map((message) => {
const json = JSON.parse(message.data) as Jsonify<TTo>;
return this.deriveDefinition.deserialize(json);
}),
);
};
this.state$ = defer(() => of(this.initializePort())).pipe(
switchMap(() => merge(initialStorageGet$, latestStorage$)),
switchMap(() => latestValueFromPort$(this.port)),
share({
connector: () => new ReplaySubject<TTo>(1),
resetOnRefCountZero: () =>
@ -130,28 +112,4 @@ export class ForegroundDerivedState<TTo> implements DerivedState<TTo> {
this.port = null;
this.backgroundResponses$ = null;
}
protected async getStoredValue(): Promise<{ derived: boolean; value: TTo | null }> {
if (this.memoryStorage.valuesRequireDeserialization) {
const storedJson = await this.memoryStorage.get<
Jsonify<{ derived: true; value: JsonObject }>
>(this.storageKey);
if (!storedJson?.derived) {
return { derived: false, value: null };
}
const value = this.deriveDefinition.deserialize(storedJson.value as any);
return { derived: true, value };
} else {
const stored = await this.memoryStorage.get<{ derived: true; value: TTo }>(this.storageKey);
if (!stored?.derived) {
return { derived: false, value: null };
}
return { derived: true, value: stored.value };
}
}
}

View File

@ -36,6 +36,10 @@ import { TwoFactorComponent } from "../auth/popup/two-factor.component";
import { UpdateTempPasswordComponent } from "../auth/popup/update-temp-password.component";
import { AutofillComponent } from "../autofill/popup/settings/autofill.component";
import { HeaderComponent } from "../platform/popup/header.component";
import { PopupFooterComponent } from "../platform/popup/layout/popup-footer.component";
import { PopupHeaderComponent } from "../platform/popup/layout/popup-header.component";
import { PopupPageComponent } from "../platform/popup/layout/popup-page.component";
import { PopupTabNavigationComponent } from "../platform/popup/layout/popup-tab-navigation.component";
import { FilePopoutCalloutComponent } from "../tools/popup/components/file-popout-callout.component";
import { GeneratorComponent } from "../tools/popup/generator/generator.component";
import { PasswordGeneratorHistoryComponent } from "../tools/popup/generator/password-generator-history.component";
@ -108,6 +112,10 @@ import "../platform/popup/locales";
AccountComponent,
ButtonModule,
ExportScopeCalloutComponent,
PopupPageComponent,
PopupTabNavigationComponent,
PopupFooterComponent,
PopupHeaderComponent,
],
declarations: [
ActionButtonsComponent,

View File

@ -68,7 +68,7 @@ img {
border: none;
}
a {
a:not(popup-page a, popup-tab-navigation a) {
text-decoration: none;
@include themify($themes) {
@ -171,7 +171,7 @@ cdk-virtual-scroll-viewport::-webkit-scrollbar-thumb,
}
}
header:not(bit-callout header, bit-dialog header) {
header:not(bit-callout header, bit-dialog header, popup-page header) {
height: 44px;
display: flex;
@ -448,7 +448,7 @@ app-root {
}
}
main {
main:not(popup-page main) {
position: absolute;
top: 44px;
bottom: 0;

View File

@ -2,6 +2,7 @@ import { DOCUMENT } from "@angular/common";
import { Inject, Injectable } from "@angular/core";
import { AbstractThemingService } from "@bitwarden/angular/platform/services/theming/theming.service.abstraction";
import { TwoFactorService } from "@bitwarden/common/auth/abstractions/two-factor.service";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { LogService as LogServiceAbstraction } from "@bitwarden/common/platform/abstractions/log.service";
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
@ -15,6 +16,7 @@ export class InitService {
private platformUtilsService: PlatformUtilsService,
private i18nService: I18nService,
private stateService: StateServiceAbstraction,
private twoFactorService: TwoFactorService,
private logService: LogServiceAbstraction,
private themingService: AbstractThemingService,
@Inject(DOCUMENT) private document: Document,
@ -24,6 +26,7 @@ export class InitService {
return async () => {
await this.stateService.init({ runMigrations: false }); // Browser background is responsible for migrations
await this.i18nService.init();
this.twoFactorService.init();
if (!BrowserPopupUtils.inPopup(window)) {
window.document.body.classList.add("body-full");

View File

@ -15,10 +15,7 @@ import {
INTRAPROCESS_MESSAGING_SUBJECT,
} from "@bitwarden/angular/services/injection-tokens";
import { JslibServicesModule } from "@bitwarden/angular/services/jslib-services.module";
import {
AuthRequestServiceAbstraction,
LoginStrategyServiceAbstraction,
} from "@bitwarden/auth/common";
import { AuthRequestServiceAbstraction } from "@bitwarden/auth/common";
import { EventCollectionService as EventCollectionServiceAbstraction } from "@bitwarden/common/abstractions/event/event-collection.service";
import { NotificationsService } from "@bitwarden/common/abstractions/notifications.service";
import { SearchService as SearchServiceAbstraction } from "@bitwarden/common/abstractions/search.service";
@ -33,7 +30,6 @@ import { DevicesServiceAbstraction } from "@bitwarden/common/auth/abstractions/d
import { KeyConnectorService } from "@bitwarden/common/auth/abstractions/key-connector.service";
import { SsoLoginServiceAbstraction } from "@bitwarden/common/auth/abstractions/sso-login.service.abstraction";
import { TokenService } from "@bitwarden/common/auth/abstractions/token.service";
import { TwoFactorService } from "@bitwarden/common/auth/abstractions/two-factor.service";
import { UserVerificationService } from "@bitwarden/common/auth/abstractions/user-verification/user-verification.service.abstraction";
import { AuthService } from "@bitwarden/common/auth/services/auth.service";
import {
@ -168,21 +164,11 @@ const safeProviders: SafeProvider[] = [
useClass: UnauthGuardService,
deps: [AuthServiceAbstraction, Router],
}),
safeProvider({
provide: TwoFactorService,
useFactory: getBgService<TwoFactorService>("twoFactorService"),
deps: [],
}),
safeProvider({
provide: AuthServiceAbstraction,
useFactory: getBgService<AuthService>("authService"),
deps: [],
}),
safeProvider({
provide: LoginStrategyServiceAbstraction,
useFactory: getBgService<LoginStrategyServiceAbstraction>("loginStrategyService"),
deps: [],
}),
safeProvider({
provide: SsoLoginServiceAbstraction,
useFactory: getBgService<SsoLoginServiceAbstraction>("ssoLoginService"),
@ -487,7 +473,7 @@ const safeProviders: SafeProvider[] = [
safeProvider({
provide: DerivedStateProvider,
useClass: ForegroundDerivedStateProvider,
deps: [StorageServiceProvider, NgZone],
deps: [NgZone],
}),
safeProvider({
provide: AutofillSettingsServiceAbstraction,

View File

@ -5,7 +5,7 @@
<div bitDialogTitle>Bitwarden</div>
<div bitDialogContent>
<p>&copy; Bitwarden Inc. 2015-{{ year }}</p>
<p>{{ "version" | i18n }}: {{ version }}</p>
<p>{{ "version" | i18n }}: {{ version$ | async }}</p>
<ng-container *ngIf="data$ | async as data">
<p *ngIf="data.isCloud">
{{ "serverVersion" | i18n }}: {{ data.serverConfig?.version }}

View File

@ -1,14 +1,13 @@
import { CommonModule } from "@angular/common";
import { Component } from "@angular/core";
import { combineLatest, map } from "rxjs";
import { Observable, combineLatest, defer, map } from "rxjs";
import { JslibModule } from "@bitwarden/angular/jslib.module";
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service";
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
import { ButtonModule, DialogModule } from "@bitwarden/components";
import { BrowserApi } from "../../platform/browser/browser-api";
@Component({
templateUrl: "about.component.html",
standalone: true,
@ -16,7 +15,7 @@ import { BrowserApi } from "../../platform/browser/browser-api";
})
export class AboutComponent {
protected year = new Date().getFullYear();
protected version = BrowserApi.getApplicationVersion();
protected version$: Observable<string>;
protected data$ = combineLatest([
this.configService.serverConfig$,
@ -26,5 +25,8 @@ export class AboutComponent {
constructor(
private configService: ConfigService,
private environmentService: EnvironmentService,
) {}
private platformUtilsService: PlatformUtilsService,
) {
this.version$ = defer(() => this.platformUtilsService.getApplicationVersion());
}
}

View File

@ -183,7 +183,7 @@ class FilelessImporterBackground implements FilelessImporterBackgroundInterface
return;
}
const filelessImportFeatureFlagEnabled = await this.configService.getFeatureFlag<boolean>(
const filelessImportFeatureFlagEnabled = await this.configService.getFeatureFlag(
FeatureFlag.BrowserFilelessImport,
);
const userAuthStatus = await this.authService.getAuthStatus();

View File

@ -46,7 +46,9 @@ export class BrowserSendStateService {
* the send component on the browser
*/
async setBrowserSendComponentState(value: BrowserSendComponentState): Promise<void> {
await this.activeUserBrowserSendComponentState.update(() => value);
await this.activeUserBrowserSendComponentState.update(() => value, {
shouldUpdate: (current) => !(current == null && value == null),
});
}
/** Get the active user's browser component state
@ -60,6 +62,8 @@ export class BrowserSendStateService {
* @param { BrowserComponentState } value set the scroll position and search text for the send component on the browser
*/
async setBrowserSendTypeComponentState(value: BrowserComponentState): Promise<void> {
await this.activeUserBrowserSendTypeComponentState.update(() => value);
await this.activeUserBrowserSendTypeComponentState.update(() => value, {
shouldUpdate: (current) => !(current == null && value == null),
});
}
}

View File

@ -292,6 +292,8 @@ export class CurrentTabComponent implements OnInit, OnDestroy {
const ciphers = await this.cipherService.getAllDecryptedForUrl(
this.url,
otherTypes.length > 0 ? otherTypes : null,
null,
false,
);
this.loginCiphers = [];

View File

@ -52,7 +52,9 @@ export class VaultBrowserStateService {
}
async setBrowserGroupingsComponentState(value: BrowserGroupingsComponentState): Promise<void> {
await this.activeUserVaultBrowserGroupingsComponentState.update(() => value);
await this.activeUserVaultBrowserGroupingsComponentState.update(() => value, {
shouldUpdate: (current) => !(current == null && value == null),
});
}
async getBrowserVaultItemsComponentState(): Promise<BrowserComponentState> {
@ -60,6 +62,8 @@ export class VaultBrowserStateService {
}
async setBrowserVaultItemsComponentState(value: BrowserComponentState): Promise<void> {
await this.activeUserVaultBrowserComponentState.update(() => value);
await this.activeUserVaultBrowserComponentState.update(() => value, {
shouldUpdate: (current) => !(current == null && value == null),
});
}
}

View File

@ -2,6 +2,7 @@
"compilerOptions": {
"moduleResolution": "node",
"noImplicitAny": true,
"allowSyntheticDefaultImports": true,
"emitDecoratorMetadata": true,
"experimentalDecorators": true,
"module": "ES2020",

View File

@ -16,6 +16,7 @@ import { OrganizationService } from "@bitwarden/common/admin-console/abstraction
import { PolicyApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/policy/policy-api.service.abstraction";
import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction";
import { AuthService } from "@bitwarden/common/auth/abstractions/auth.service";
import { KdfConfigService } from "@bitwarden/common/auth/abstractions/kdf-config.service";
import { KeyConnectorService } from "@bitwarden/common/auth/abstractions/key-connector.service";
import { TwoFactorService } from "@bitwarden/common/auth/abstractions/two-factor.service";
import { TwoFactorProviderType } from "@bitwarden/common/auth/enums/two-factor-provider-type";
@ -68,6 +69,7 @@ export class LoginCommand {
protected policyApiService: PolicyApiServiceAbstraction,
protected orgService: OrganizationService,
protected logoutCallback: () => Promise<void>,
protected kdfConfigService: KdfConfigService,
) {}
async run(email: string, password: string, options: OptionValues) {
@ -229,7 +231,7 @@ export class LoginCommand {
}
}
if (response.requiresTwoFactor) {
const twoFactorProviders = this.twoFactorService.getSupportedProviders(null);
const twoFactorProviders = await this.twoFactorService.getSupportedProviders(null);
if (twoFactorProviders.length === 0) {
return Response.badRequest("No providers available for this client.");
}
@ -270,7 +272,7 @@ export class LoginCommand {
if (
twoFactorToken == null &&
response.twoFactorProviders.size > 1 &&
Object.keys(response.twoFactorProviders).length > 1 &&
selectedProvider.type === TwoFactorProviderType.Email
) {
const emailReq = new TwoFactorEmailRequest();
@ -563,14 +565,12 @@ export class LoginCommand {
message: "Master Password Hint (optional):",
});
const masterPasswordHint = hint.input;
const kdf = await this.stateService.getKdfType();
const kdfConfig = await this.stateService.getKdfConfig();
const kdfConfig = await this.kdfConfigService.getKdfConfig();
// Create new key and hash new password
const newMasterKey = await this.cryptoService.makeMasterKey(
masterPassword,
this.email.trim().toLowerCase(),
kdf,
kdfConfig,
);
const newPasswordHash = await this.cryptoService.hashMasterKey(masterPassword, newMasterKey);

View File

@ -3,6 +3,7 @@ import { firstValueFrom } from "rxjs";
import { ApiService } from "@bitwarden/common/abstractions/api.service";
import { OrganizationApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/organization/organization-api.service.abstraction";
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
import { KdfConfigService } from "@bitwarden/common/auth/abstractions/kdf-config.service";
import { KeyConnectorService } from "@bitwarden/common/auth/abstractions/key-connector.service";
import { InternalMasterPasswordServiceAbstraction } from "@bitwarden/common/auth/abstractions/master-password.service.abstraction";
import { SecretVerificationRequest } from "@bitwarden/common/auth/models/request/secret-verification.request";
@ -34,6 +35,7 @@ export class UnlockCommand {
private syncService: SyncService,
private organizationApiService: OrganizationApiServiceAbstraction,
private logout: () => Promise<void>,
private kdfConfigService: KdfConfigService,
) {}
async run(password: string, cmdOptions: Record<string, any>) {
@ -48,9 +50,8 @@ export class UnlockCommand {
await this.setNewSessionKey();
const email = await this.stateService.getEmail();
const kdf = await this.stateService.getKdfType();
const kdfConfig = await this.stateService.getKdfConfig();
const masterKey = await this.cryptoService.makeMasterKey(password, email, kdf, kdfConfig);
const kdfConfig = await this.kdfConfigService.getKdfConfig();
const masterKey = await this.cryptoService.makeMasterKey(password, email, kdfConfig);
const userId = (await firstValueFrom(this.accountService.activeAccount$))?.id;
const storedMasterKeyHash = await firstValueFrom(
this.masterPasswordService.masterKeyHash$(userId),

View File

@ -30,12 +30,14 @@ import { AccountService } from "@bitwarden/common/auth/abstractions/account.serv
import { AvatarService as AvatarServiceAbstraction } from "@bitwarden/common/auth/abstractions/avatar.service";
import { DeviceTrustServiceAbstraction } from "@bitwarden/common/auth/abstractions/device-trust.service.abstraction";
import { DevicesApiServiceAbstraction } from "@bitwarden/common/auth/abstractions/devices-api.service.abstraction";
import { KdfConfigService as KdfConfigServiceAbstraction } from "@bitwarden/common/auth/abstractions/kdf-config.service";
import { InternalMasterPasswordServiceAbstraction } from "@bitwarden/common/auth/abstractions/master-password.service.abstraction";
import { AccountServiceImplementation } from "@bitwarden/common/auth/services/account.service";
import { AuthService } from "@bitwarden/common/auth/services/auth.service";
import { AvatarService } from "@bitwarden/common/auth/services/avatar.service";
import { DeviceTrustService } from "@bitwarden/common/auth/services/device-trust.service.implementation";
import { DevicesApiServiceImplementation } from "@bitwarden/common/auth/services/devices-api.service.implementation";
import { KdfConfigService } from "@bitwarden/common/auth/services/kdf-config.service";
import { KeyConnectorService } from "@bitwarden/common/auth/services/key-connector.service";
import { MasterPasswordService } from "@bitwarden/common/auth/services/master-password/master-password.service";
import { TokenService } from "@bitwarden/common/auth/services/token.service";
@ -235,6 +237,7 @@ export class Main {
billingAccountProfileStateService: BillingAccountProfileStateService;
providerApiService: ProviderApiServiceAbstraction;
userKeyInitService: UserKeyInitService;
kdfConfigService: KdfConfigServiceAbstraction;
constructor() {
let p = null;
@ -311,7 +314,7 @@ export class Main {
this.singleUserStateProvider,
);
this.derivedStateProvider = new DefaultDerivedStateProvider(storageServiceProvider);
this.derivedStateProvider = new DefaultDerivedStateProvider();
this.stateProvider = new DefaultStateProvider(
this.activeUserStateProvider,
@ -357,6 +360,8 @@ export class Main {
this.masterPasswordService = new MasterPasswordService(this.stateProvider);
this.kdfConfigService = new KdfConfigService(this.stateProvider);
this.cryptoService = new CryptoService(
this.masterPasswordService,
this.keyGenerationService,
@ -367,6 +372,7 @@ export class Main {
this.stateService,
this.accountService,
this.stateProvider,
this.kdfConfigService,
);
this.appIdService = new AppIdService(this.globalStateProvider);
@ -449,7 +455,11 @@ export class Main {
this.stateProvider,
);
this.twoFactorService = new TwoFactorService(this.i18nService, this.platformUtilsService);
this.twoFactorService = new TwoFactorService(
this.i18nService,
this.platformUtilsService,
this.globalStateProvider,
);
this.passwordStrengthService = new PasswordStrengthService();
@ -512,6 +522,7 @@ export class Main {
this.userDecryptionOptionsService,
this.globalStateProvider,
this.billingAccountProfileStateService,
this.kdfConfigService,
);
this.authService = new AuthService(
@ -574,6 +585,7 @@ export class Main {
this.cryptoService,
this.vaultTimeoutSettingsService,
this.logService,
this.kdfConfigService,
);
this.userVerificationService = new UserVerificationService(
@ -588,6 +600,7 @@ export class Main {
this.logService,
this.vaultTimeoutSettingsService,
this.platformUtilsService,
this.kdfConfigService,
);
this.vaultTimeoutService = new VaultTimeoutService(
@ -654,7 +667,7 @@ export class Main {
this.cipherService,
this.cryptoService,
this.cryptoFunctionService,
this.stateService,
this.kdfConfigService,
);
this.organizationExportService = new OrganizationVaultExportService(
@ -662,8 +675,8 @@ export class Main {
this.apiService,
this.cryptoService,
this.cryptoFunctionService,
this.stateService,
this.collectionService,
this.kdfConfigService,
);
this.exportService = new VaultExportService(

View File

@ -134,6 +134,7 @@ export class ServeCommand {
this.main.syncService,
this.main.organizationApiService,
async () => await this.main.logout(),
this.main.kdfConfigService,
);
this.sendCreateCommand = new SendCreateCommand(

View File

@ -156,6 +156,7 @@ export class Program {
this.main.policyApiService,
this.main.organizationService,
async () => await this.main.logout(),
this.main.kdfConfigService,
);
const response = await command.run(email, password, options);
this.processResponse(response, true);
@ -265,6 +266,7 @@ export class Program {
this.main.syncService,
this.main.organizationApiService,
async () => await this.main.logout(),
this.main.kdfConfigService,
);
const response = await command.run(password, cmd);
this.processResponse(response);
@ -627,6 +629,7 @@ export class Program {
this.main.syncService,
this.main.organizationApiService,
this.main.logout,
this.main.kdfConfigService,
);
const response = await command.run(null, null);
if (!response.success) {

View File

@ -21,6 +21,7 @@ import { VaultTimeoutSettingsService } from "@bitwarden/common/abstractions/vaul
import { PolicyService as PolicyServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction";
import { AccountService as AccountServiceAbstraction } from "@bitwarden/common/auth/abstractions/account.service";
import { AuthService as AuthServiceAbstraction } from "@bitwarden/common/auth/abstractions/auth.service";
import { KdfConfigService as KdfConfigServiceAbstraction } from "@bitwarden/common/auth/abstractions/kdf-config.service";
import { InternalMasterPasswordServiceAbstraction } from "@bitwarden/common/auth/abstractions/master-password.service.abstraction";
import { TokenService } from "@bitwarden/common/auth/abstractions/token.service";
import { AutofillSettingsServiceAbstraction } from "@bitwarden/common/autofill/services/autofill-settings.service";
@ -258,6 +259,7 @@ const safeProviders: SafeProvider[] = [
AccountServiceAbstraction,
StateProvider,
BiometricStateService,
KdfConfigServiceAbstraction,
],
}),
safeProvider({

View File

@ -14,6 +14,7 @@ import { PolicyApiServiceAbstraction } from "@bitwarden/common/admin-console/abs
import { InternalPolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction";
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
import { DeviceTrustServiceAbstraction } from "@bitwarden/common/auth/abstractions/device-trust.service.abstraction";
import { KdfConfigService } from "@bitwarden/common/auth/abstractions/kdf-config.service";
import { InternalMasterPasswordServiceAbstraction } from "@bitwarden/common/auth/abstractions/master-password.service.abstraction";
import { UserVerificationService } from "@bitwarden/common/auth/abstractions/user-verification/user-verification.service.abstraction";
import { FakeMasterPasswordService } from "@bitwarden/common/auth/services/master-password/fake-master-password.service";
@ -164,6 +165,10 @@ describe("LockComponent", () => {
provide: AccountService,
useValue: accountService,
},
{
provide: KdfConfigService,
useValue: mock<KdfConfigService>(),
},
],
schemas: [NO_ERRORS_SCHEMA],
}).compileComponents();

View File

@ -11,6 +11,7 @@ import { PolicyApiServiceAbstraction } from "@bitwarden/common/admin-console/abs
import { InternalPolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction";
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
import { DeviceTrustServiceAbstraction } from "@bitwarden/common/auth/abstractions/device-trust.service.abstraction";
import { KdfConfigService } from "@bitwarden/common/auth/abstractions/kdf-config.service";
import { InternalMasterPasswordServiceAbstraction } from "@bitwarden/common/auth/abstractions/master-password.service.abstraction";
import { UserVerificationService } from "@bitwarden/common/auth/abstractions/user-verification/user-verification.service.abstraction";
import { DeviceType } from "@bitwarden/common/enums";
@ -63,6 +64,7 @@ export class LockComponent extends BaseLockComponent {
pinCryptoService: PinCryptoServiceAbstraction,
biometricStateService: BiometricStateService,
accountService: AccountService,
kdfConfigService: KdfConfigService,
) {
super(
masterPasswordService,
@ -87,6 +89,7 @@ export class LockComponent extends BaseLockComponent {
pinCryptoService,
biometricStateService,
accountService,
kdfConfigService,
);
}

View File

@ -9,6 +9,7 @@ import { OrganizationUserService } from "@bitwarden/common/admin-console/abstrac
import { PolicyApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/policy/policy-api.service.abstraction";
import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction";
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
import { KdfConfigService } from "@bitwarden/common/auth/abstractions/kdf-config.service";
import { InternalMasterPasswordServiceAbstraction } from "@bitwarden/common/auth/abstractions/master-password.service.abstraction";
import { SsoLoginServiceAbstraction } from "@bitwarden/common/auth/abstractions/sso-login.service.abstraction";
import { BroadcasterService } from "@bitwarden/common/platform/abstractions/broadcaster.service";
@ -52,6 +53,7 @@ export class SetPasswordComponent extends BaseSetPasswordComponent implements On
userDecryptionOptionsService: InternalUserDecryptionOptionsServiceAbstraction,
ssoLoginService: SsoLoginServiceAbstraction,
dialogService: DialogService,
kdfConfigService: KdfConfigService,
) {
super(
accountService,
@ -73,6 +75,7 @@ export class SetPasswordComponent extends BaseSetPasswordComponent implements On
userDecryptionOptionsService,
ssoLoginService,
dialogService,
kdfConfigService,
);
}

View File

@ -1636,7 +1636,7 @@
"message": "Error enabling browser integration"
},
"browserIntegrationErrorDesc": {
"message": "An error has occurred while enabling browser integration."
"message": "Une erreur s'est produite lors de l'action de l'intégration du navigateur."
},
"browserIntegrationMasOnlyDesc": {
"message": "Malheureusement l'intégration avec le navigateur est uniquement supportée dans la version Mac App Store pour le moment."
@ -2698,7 +2698,7 @@
"description": "Label indicating the most common import formats"
},
"success": {
"message": "Success"
"message": "Succès"
},
"troubleshooting": {
"message": "Résolution de problèmes"

View File

@ -2698,7 +2698,7 @@
"description": "Label indicating the most common import formats"
},
"success": {
"message": "Success"
"message": "Sikeres"
},
"troubleshooting": {
"message": "Hibaelhárítás"

View File

@ -2698,7 +2698,7 @@
"description": "Label indicating the most common import formats"
},
"success": {
"message": "Success"
"message": "Izdevās"
},
"troubleshooting": {
"message": "Sarežģījumu novēršana"

View File

@ -2698,7 +2698,7 @@
"description": "Label indicating the most common import formats"
},
"success": {
"message": "Success"
"message": "Succes"
},
"troubleshooting": {
"message": "Probleemoplossing"

View File

@ -2698,7 +2698,7 @@
"description": "Label indicating the most common import formats"
},
"success": {
"message": "Success"
"message": "成功"
},
"troubleshooting": {
"message": "故障排除"

View File

@ -157,7 +157,7 @@ export class Main {
activeUserStateProvider,
singleUserStateProvider,
globalStateProvider,
new DefaultDerivedStateProvider(storageServiceProvider),
new DefaultDerivedStateProvider(),
);
this.environmentService = new DefaultEnvironmentService(stateProvider, accountService);

View File

@ -1,6 +1,7 @@
import { FakeStateProvider } from "@bitwarden/common/../spec/fake-state-provider";
import { mock } from "jest-mock-extended";
import { KdfConfigService } from "@bitwarden/common/auth/abstractions/kdf-config.service";
import { FakeMasterPasswordService } from "@bitwarden/common/auth/services/master-password/fake-master-password.service";
import { CryptoFunctionService } from "@bitwarden/common/platform/abstractions/crypto-function.service";
import { EncryptService } from "@bitwarden/common/platform/abstractions/encrypt.service";
@ -35,6 +36,7 @@ describe("electronCryptoService", () => {
let accountService: FakeAccountService;
let stateProvider: FakeStateProvider;
const biometricStateService = mock<BiometricStateService>();
const kdfConfigService = mock<KdfConfigService>();
const mockUserId = "mock user id" as UserId;
@ -54,6 +56,7 @@ describe("electronCryptoService", () => {
accountService,
stateProvider,
biometricStateService,
kdfConfigService,
);
});

View File

@ -1,6 +1,7 @@
import { firstValueFrom } from "rxjs";
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
import { KdfConfigService } from "@bitwarden/common/auth/abstractions/kdf-config.service";
import { InternalMasterPasswordServiceAbstraction } from "@bitwarden/common/auth/abstractions/master-password.service.abstraction";
import { CryptoFunctionService } from "@bitwarden/common/platform/abstractions/crypto-function.service";
import { EncryptService } from "@bitwarden/common/platform/abstractions/encrypt.service";
@ -31,6 +32,7 @@ export class ElectronCryptoService extends CryptoService {
accountService: AccountService,
stateProvider: StateProvider,
private biometricStateService: BiometricStateService,
kdfConfigService: KdfConfigService,
) {
super(
masterPasswordService,
@ -42,6 +44,7 @@ export class ElectronCryptoService extends CryptoService {
stateService,
accountService,
stateProvider,
kdfConfigService,
);
}

View File

@ -110,7 +110,7 @@
></bit-nav-item>
</bit-nav-group>
<navigation-product-switcher></navigation-product-switcher>
<navigation-product-switcher class="tw-mt-auto"></navigation-product-switcher>
<app-toggle-width></app-toggle-width>
</nav>

View File

@ -60,7 +60,6 @@ export class OrganizationLayoutComponent implements OnInit, OnDestroy {
protected showPaymentMethodWarningBanners$ = this.configService.getFeatureFlag$(
FeatureFlag.ShowPaymentMethodWarningBanners,
false,
);
constructor(

View File

@ -194,7 +194,7 @@ export class GroupAddEditComponent implements OnInit, OnDestroy {
}),
);
}),
shareReplay({ refCount: false }),
shareReplay({ refCount: true, bufferSize: 1 }),
);
restrictGroupAccess$ = combineLatest([

View File

@ -218,7 +218,6 @@ export class MemberDialogComponent implements OnDestroy {
groups: groups$,
flexibleCollectionsV1Enabled: this.configService.getFeatureFlag$(
FeatureFlag.FlexibleCollectionsV1,
false,
),
})
.pipe(takeUntil(this.destroy$))

View File

@ -7,10 +7,15 @@ import {
OrganizationUserResetPasswordRequest,
OrganizationUserResetPasswordWithIdRequest,
} from "@bitwarden/common/admin-console/abstractions/organization-user/requests";
import { KdfConfig } from "@bitwarden/common/auth/models/domain/kdf-config";
import {
Argon2KdfConfig,
KdfConfig,
PBKDF2KdfConfig,
} from "@bitwarden/common/auth/models/domain/kdf-config";
import { CryptoService } from "@bitwarden/common/platform/abstractions/crypto.service";
import { EncryptService } from "@bitwarden/common/platform/abstractions/encrypt.service";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { KdfType } from "@bitwarden/common/platform/enums";
import { Utils } from "@bitwarden/common/platform/misc/utils";
import { EncryptedString, EncString } from "@bitwarden/common/platform/models/domain/enc-string";
import { SymmetricCryptoKey } from "@bitwarden/common/platform/models/domain/symmetric-crypto-key";
@ -90,12 +95,17 @@ export class OrganizationUserResetPasswordService {
const decValue = await this.cryptoService.rsaDecrypt(response.resetPasswordKey, decPrivateKey);
const existingUserKey = new SymmetricCryptoKey(decValue) as UserKey;
// determine Kdf Algorithm
const kdfConfig: KdfConfig =
response.kdf === KdfType.PBKDF2_SHA256
? new PBKDF2KdfConfig(response.kdfIterations)
: new Argon2KdfConfig(response.kdfIterations, response.kdfMemory, response.kdfParallelism);
// Create new master key and hash new password
const newMasterKey = await this.cryptoService.makeMasterKey(
newMasterPassword,
email.trim().toLowerCase(),
response.kdf,
new KdfConfig(response.kdfIterations, response.kdfMemory, response.kdfParallelism),
kdfConfig,
);
const newMasterKeyHash = await this.cryptoService.hashMasterKey(
newMasterPassword,

View File

@ -44,12 +44,10 @@ export class AccountComponent {
protected flexibleCollectionsMigrationEnabled$ = this.configService.getFeatureFlag$(
FeatureFlag.FlexibleCollectionsMigration,
false,
);
flexibleCollectionsV1Enabled$ = this.configService.getFeatureFlag$(
FeatureFlag.FlexibleCollectionsV1,
false,
);
// FormGroup validators taken from server Organization domain object

View File

@ -10,6 +10,7 @@ import { PolicyService } from "@bitwarden/common/admin-console/abstractions/poli
import { TwoFactorProviderType } from "@bitwarden/common/auth/enums/two-factor-provider-type";
import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions/account/billing-account-profile-state.service";
import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service";
import { DialogService } from "@bitwarden/components";
import { TwoFactorDuoComponent } from "../../../auth/settings/two-factor-duo.component";
import { TwoFactorSetupComponent as BaseTwoFactorSetupComponent } from "../../../auth/settings/two-factor-setup.component";
@ -22,6 +23,7 @@ import { TwoFactorSetupComponent as BaseTwoFactorSetupComponent } from "../../..
export class TwoFactorSetupComponent extends BaseTwoFactorSetupComponent {
tabbedHeader = false;
constructor(
dialogService: DialogService,
apiService: ApiService,
modalService: ModalService,
messagingService: MessagingService,
@ -31,6 +33,7 @@ export class TwoFactorSetupComponent extends BaseTwoFactorSetupComponent {
billingAccountProfileStateService: BillingAccountProfileStateService,
) {
super(
dialogService,
apiService,
modalService,
messagingService,

View File

@ -3,10 +3,15 @@ import { Injectable } from "@angular/core";
import { ApiService } from "@bitwarden/common/abstractions/api.service";
import { PolicyData } from "@bitwarden/common/admin-console/models/data/policy.data";
import { Policy } from "@bitwarden/common/admin-console/models/domain/policy";
import { KdfConfig } from "@bitwarden/common/auth/models/domain/kdf-config";
import {
Argon2KdfConfig,
KdfConfig,
PBKDF2KdfConfig,
} from "@bitwarden/common/auth/models/domain/kdf-config";
import { CryptoService } from "@bitwarden/common/platform/abstractions/crypto.service";
import { EncryptService } from "@bitwarden/common/platform/abstractions/encrypt.service";
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
import { KdfType } from "@bitwarden/common/platform/enums";
import { Utils } from "@bitwarden/common/platform/misc/utils";
import { EncryptedString } from "@bitwarden/common/platform/models/domain/enc-string";
import { SymmetricCryptoKey } from "@bitwarden/common/platform/models/domain/symmetric-crypto-key";
@ -231,16 +236,22 @@ export class EmergencyAccessService {
const grantorUserKey = new SymmetricCryptoKey(grantorKeyBuffer) as UserKey;
const masterKey = await this.cryptoService.makeMasterKey(
masterPassword,
email,
takeoverResponse.kdf,
new KdfConfig(
takeoverResponse.kdfIterations,
takeoverResponse.kdfMemory,
takeoverResponse.kdfParallelism,
),
);
let config: KdfConfig;
switch (takeoverResponse.kdf) {
case KdfType.PBKDF2_SHA256:
config = new PBKDF2KdfConfig(takeoverResponse.kdfIterations);
break;
case KdfType.Argon2id:
config = new Argon2KdfConfig(
takeoverResponse.kdfIterations,
takeoverResponse.kdfMemory,
takeoverResponse.kdfParallelism,
);
break;
}
const masterKey = await this.cryptoService.makeMasterKey(masterPassword, email, config);
const masterKeyHash = await this.cryptoService.hashMasterKey(masterPassword, masterKey);
const encKey = await this.cryptoService.encryptUserKeyWithMasterKey(masterKey, grantorUserKey);

View File

@ -2,6 +2,7 @@ import { mock, MockProxy } from "jest-mock-extended";
import { BehaviorSubject } from "rxjs";
import { DeviceTrustServiceAbstraction } from "@bitwarden/common/auth/abstractions/device-trust.service.abstraction";
import { KdfConfigService } from "@bitwarden/common/auth/abstractions/kdf-config.service";
import { FakeMasterPasswordService } from "@bitwarden/common/auth/services/master-password/fake-master-password.service";
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
import { CryptoService } from "@bitwarden/common/platform/abstractions/crypto.service";
@ -47,6 +48,7 @@ describe("KeyRotationService", () => {
let mockEncryptService: MockProxy<EncryptService>;
let mockStateService: MockProxy<StateService>;
let mockConfigService: MockProxy<ConfigService>;
let mockKdfConfigService: MockProxy<KdfConfigService>;
const mockUserId = Utils.newGuid() as UserId;
const mockAccountService: FakeAccountService = mockAccountServiceWith(mockUserId);
@ -65,6 +67,7 @@ describe("KeyRotationService", () => {
mockEncryptService = mock<EncryptService>();
mockStateService = mock<StateService>();
mockConfigService = mock<ConfigService>();
mockKdfConfigService = mock<KdfConfigService>();
keyRotationService = new UserKeyRotationService(
mockMasterPasswordService,
@ -80,6 +83,7 @@ describe("KeyRotationService", () => {
mockStateService,
mockAccountService,
mockConfigService,
mockKdfConfigService,
);
});

View File

@ -3,6 +3,7 @@ import { firstValueFrom } from "rxjs";
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
import { DeviceTrustServiceAbstraction } from "@bitwarden/common/auth/abstractions/device-trust.service.abstraction";
import { KdfConfigService } from "@bitwarden/common/auth/abstractions/kdf-config.service";
import { InternalMasterPasswordServiceAbstraction } from "@bitwarden/common/auth/abstractions/master-password.service.abstraction";
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
@ -39,6 +40,7 @@ export class UserKeyRotationService {
private stateService: StateService,
private accountService: AccountService,
private configService: ConfigService,
private kdfConfigService: KdfConfigService,
) {}
/**
@ -54,8 +56,7 @@ export class UserKeyRotationService {
const masterKey = await this.cryptoService.makeMasterKey(
masterPassword,
await this.stateService.getEmail(),
await this.stateService.getKdfType(),
await this.stateService.getKdfConfig(),
await this.kdfConfigService.getKdfConfig(),
);
if (!masterKey) {
@ -89,7 +90,7 @@ export class UserKeyRotationService {
request.emergencyAccessKeys = await this.emergencyAccessService.getRotatedKeys(newUserKey);
request.resetPasswordKeys = await this.resetPasswordService.getRotatedKeys(newUserKey);
if (await this.configService.getFeatureFlag<boolean>(FeatureFlag.KeyRotationImprovements)) {
if (await this.configService.getFeatureFlag(FeatureFlag.KeyRotationImprovements)) {
await this.apiService.postUserKeyUpdate(request);
} else {
await this.rotateUserKeyAndEncryptedDataLegacy(request);

View File

@ -2,6 +2,7 @@ import { Component, OnInit } from "@angular/core";
import { FormBuilder, Validators } from "@angular/forms";
import { ApiService } from "@bitwarden/common/abstractions/api.service";
import { KdfConfigService } from "@bitwarden/common/auth/abstractions/kdf-config.service";
import { TwoFactorProviderType } from "@bitwarden/common/auth/enums/two-factor-provider-type";
import { EmailTokenRequest } from "@bitwarden/common/auth/models/request/email-token.request";
import { EmailRequest } from "@bitwarden/common/auth/models/request/email.request";
@ -37,6 +38,7 @@ export class ChangeEmailComponent implements OnInit {
private logService: LogService,
private stateService: StateService,
private formBuilder: FormBuilder,
private kdfConfigService: KdfConfigService,
) {}
async ngOnInit() {
@ -83,12 +85,10 @@ export class ChangeEmailComponent implements OnInit {
step1Value.masterPassword,
await this.cryptoService.getOrDeriveMasterKey(step1Value.masterPassword),
);
const kdf = await this.stateService.getKdfType();
const kdfConfig = await this.stateService.getKdfConfig();
const kdfConfig = await this.kdfConfigService.getKdfConfig();
const newMasterKey = await this.cryptoService.makeMasterKey(
step1Value.masterPassword,
newEmail,
kdf,
kdfConfig,
);
request.newMasterPasswordHash = await this.cryptoService.hashMasterKey(

View File

@ -5,6 +5,7 @@ import { ChangePasswordComponent as BaseChangePasswordComponent } from "@bitward
import { ApiService } from "@bitwarden/common/abstractions/api.service";
import { AuditService } from "@bitwarden/common/abstractions/audit.service";
import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction";
import { KdfConfigService } from "@bitwarden/common/auth/abstractions/kdf-config.service";
import { UserVerificationService } from "@bitwarden/common/auth/abstractions/user-verification/user-verification.service.abstraction";
import { PasswordRequest } from "@bitwarden/common/auth/models/request/password.request";
import { CryptoService } from "@bitwarden/common/platform/abstractions/crypto.service";
@ -48,6 +49,7 @@ export class ChangePasswordComponent extends BaseChangePasswordComponent {
dialogService: DialogService,
private userVerificationService: UserVerificationService,
private keyRotationService: UserKeyRotationService,
kdfConfigService: KdfConfigService,
) {
super(
i18nService,
@ -58,6 +60,7 @@ export class ChangePasswordComponent extends BaseChangePasswordComponent {
policyService,
stateService,
dialogService,
kdfConfigService,
);
}

View File

@ -5,6 +5,7 @@ import { takeUntil } from "rxjs";
import { ChangePasswordComponent } from "@bitwarden/angular/auth/components/change-password.component";
import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction";
import { KdfConfigService } from "@bitwarden/common/auth/abstractions/kdf-config.service";
import { CryptoService } from "@bitwarden/common/platform/abstractions/crypto.service";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
@ -58,6 +59,7 @@ export class EmergencyAccessTakeoverComponent
private logService: LogService,
dialogService: DialogService,
private dialogRef: DialogRef<EmergencyAccessTakeoverResultType>,
kdfConfigService: KdfConfigService,
) {
super(
i18nService,
@ -68,6 +70,7 @@ export class EmergencyAccessTakeoverComponent
policyService,
stateService,
dialogService,
kdfConfigService,
);
}

View File

@ -3,6 +3,7 @@ import { Component, Inject } from "@angular/core";
import { FormGroup, FormControl, Validators } from "@angular/forms";
import { ApiService } from "@bitwarden/common/abstractions/api.service";
import { KdfConfigService } from "@bitwarden/common/auth/abstractions/kdf-config.service";
import { KdfConfig } from "@bitwarden/common/auth/models/domain/kdf-config";
import { KdfRequest } from "@bitwarden/common/models/request/kdf.request";
import { CryptoService } from "@bitwarden/common/platform/abstractions/crypto.service";
@ -18,7 +19,6 @@ import { KdfType } from "@bitwarden/common/platform/enums";
templateUrl: "change-kdf-confirmation.component.html",
})
export class ChangeKdfConfirmationComponent {
kdf: KdfType;
kdfConfig: KdfConfig;
form = new FormGroup({
@ -37,9 +37,9 @@ export class ChangeKdfConfirmationComponent {
private messagingService: MessagingService,
private stateService: StateService,
private logService: LogService,
@Inject(DIALOG_DATA) params: { kdf: KdfType; kdfConfig: KdfConfig },
private kdfConfigService: KdfConfigService,
@Inject(DIALOG_DATA) params: { kdfConfig: KdfConfig },
) {
this.kdf = params.kdf;
this.kdfConfig = params.kdfConfig;
this.masterPassword = null;
}
@ -65,22 +65,24 @@ export class ChangeKdfConfirmationComponent {
private async makeKeyAndSaveAsync() {
const masterPassword = this.form.value.masterPassword;
// Ensure the KDF config is valid.
this.kdfConfig.validateKdfConfig();
const request = new KdfRequest();
request.kdf = this.kdf;
request.kdf = this.kdfConfig.kdfType;
request.kdfIterations = this.kdfConfig.iterations;
request.kdfMemory = this.kdfConfig.memory;
request.kdfParallelism = this.kdfConfig.parallelism;
if (this.kdfConfig.kdfType === KdfType.Argon2id) {
request.kdfMemory = this.kdfConfig.memory;
request.kdfParallelism = this.kdfConfig.parallelism;
}
const masterKey = await this.cryptoService.getOrDeriveMasterKey(masterPassword);
request.masterPasswordHash = await this.cryptoService.hashMasterKey(masterPassword, masterKey);
const email = await this.stateService.getEmail();
// Ensure the KDF config is valid.
this.cryptoService.validateKdfConfig(this.kdf, this.kdfConfig);
const newMasterKey = await this.cryptoService.makeMasterKey(
masterPassword,
email,
this.kdf,
this.kdfConfig,
);
request.newMasterPasswordHash = await this.cryptoService.hashMasterKey(

View File

@ -19,14 +19,14 @@
<select
id="kdf"
name="Kdf"
[(ngModel)]="kdf"
[(ngModel)]="kdfConfig.kdfType"
(ngModelChange)="onChangeKdf($event)"
class="form-control mb-3"
required
>
<option *ngFor="let o of kdfOptions" [ngValue]="o.value">{{ o.name }}</option>
</select>
<ng-container *ngIf="kdf == kdfType.Argon2id">
<ng-container *ngIf="isArgon2(kdfConfig)">
<label for="kdfMemory">{{ "kdfMemory" | i18n }}</label>
<input
id="kdfMemory"
@ -43,7 +43,7 @@
</div>
<div class="col-6">
<div class="form-group mb-0">
<ng-container *ngIf="kdf == kdfType.PBKDF2_SHA256">
<ng-container *ngIf="isPBKDF2(kdfConfig)">
<label for="kdfIterations">{{ "kdfIterations" | i18n }}</label>
<a
class="ml-auto"
@ -65,7 +65,7 @@
required
/>
</ng-container>
<ng-container *ngIf="kdf == kdfType.Argon2id">
<ng-container *ngIf="isArgon2(kdfConfig)">
<label for="kdfIterations">{{ "kdfIterations" | i18n }}</label>
<input
id="iterations"
@ -92,7 +92,7 @@
</div>
</div>
<div class="col-12">
<ng-container *ngIf="kdf == kdfType.PBKDF2_SHA256">
<ng-container *ngIf="isPBKDF2(kdfConfig)">
<p class="small form-text text-muted">
{{ "kdfIterationsDesc" | i18n: (PBKDF2_ITERATIONS.defaultValue | number) }}
</p>
@ -100,7 +100,7 @@
{{ "kdfIterationsWarning" | i18n: (100000 | number) }}
</bit-callout>
</ng-container>
<ng-container *ngIf="kdf == kdfType.Argon2id">
<ng-container *ngIf="isArgon2(kdfConfig)">
<p class="small form-text text-muted">{{ "argon2Desc" | i18n }}</p>
<bit-callout type="warning"> {{ "argon2Warning" | i18n }}</bit-callout>
</ng-container>

View File

@ -1,7 +1,11 @@
import { Component, OnInit } from "@angular/core";
import { KdfConfig } from "@bitwarden/common/auth/models/domain/kdf-config";
import { StateService } from "@bitwarden/common/platform/abstractions/state.service";
import { KdfConfigService } from "@bitwarden/common/auth/abstractions/kdf-config.service";
import {
Argon2KdfConfig,
KdfConfig,
PBKDF2KdfConfig,
} from "@bitwarden/common/auth/models/domain/kdf-config";
import {
DEFAULT_KDF_CONFIG,
PBKDF2_ITERATIONS,
@ -19,7 +23,6 @@ import { ChangeKdfConfirmationComponent } from "./change-kdf-confirmation.compon
templateUrl: "change-kdf.component.html",
})
export class ChangeKdfComponent implements OnInit {
kdf = KdfType.PBKDF2_SHA256;
kdfConfig: KdfConfig = DEFAULT_KDF_CONFIG;
kdfType = KdfType;
kdfOptions: any[] = [];
@ -31,8 +34,8 @@ export class ChangeKdfComponent implements OnInit {
protected ARGON2_PARALLELISM = ARGON2_PARALLELISM;
constructor(
private stateService: StateService,
private dialogService: DialogService,
private kdfConfigService: KdfConfigService,
) {
this.kdfOptions = [
{ name: "PBKDF2 SHA-256", value: KdfType.PBKDF2_SHA256 },
@ -41,19 +44,22 @@ export class ChangeKdfComponent implements OnInit {
}
async ngOnInit() {
this.kdf = await this.stateService.getKdfType();
this.kdfConfig = await this.stateService.getKdfConfig();
this.kdfConfig = await this.kdfConfigService.getKdfConfig();
}
isPBKDF2(t: KdfConfig): t is PBKDF2KdfConfig {
return t instanceof PBKDF2KdfConfig;
}
isArgon2(t: KdfConfig): t is Argon2KdfConfig {
return t instanceof Argon2KdfConfig;
}
async onChangeKdf(newValue: KdfType) {
if (newValue === KdfType.PBKDF2_SHA256) {
this.kdfConfig = new KdfConfig(PBKDF2_ITERATIONS.defaultValue);
this.kdfConfig = new PBKDF2KdfConfig();
} else if (newValue === KdfType.Argon2id) {
this.kdfConfig = new KdfConfig(
ARGON2_ITERATIONS.defaultValue,
ARGON2_MEMORY.defaultValue,
ARGON2_PARALLELISM.defaultValue,
);
this.kdfConfig = new Argon2KdfConfig();
} else {
throw new Error("Unknown KDF type.");
}
@ -62,7 +68,6 @@ export class ChangeKdfComponent implements OnInit {
async openConfirmationModal() {
this.dialogService.open(ChangeKdfConfirmationComponent, {
data: {
kdf: this.kdf,
kdfConfig: this.kdfConfig,
},
});

View File

@ -15,13 +15,6 @@
<span aria-hidden="true">&times;</span>
</button>
</div>
<app-two-factor-verify
[organizationId]="organizationId"
[type]="type"
(onAuthed)="auth($any($event))"
*ngIf="!authed"
>
</app-two-factor-verify>
<form
#form
(ngSubmit)="submit()"

View File

@ -15,13 +15,6 @@
<span aria-hidden="true">&times;</span>
</button>
</div>
<app-two-factor-verify
[organizationId]="organizationId"
[type]="type"
(onAuthed)="auth($any($event))"
*ngIf="!authed"
>
</app-two-factor-verify>
<form
#form
(ngSubmit)="submit()"

View File

@ -15,13 +15,6 @@
<span aria-hidden="true">&times;</span>
</button>
</div>
<app-two-factor-verify
[organizationId]="organizationId"
[type]="type"
(onAuthed)="auth($any($event))"
*ngIf="!authed"
>
</app-two-factor-verify>
<form
#form
(ngSubmit)="submit()"

View File

@ -15,8 +15,6 @@
<span aria-hidden="true">&times;</span>
</button>
</div>
<app-two-factor-verify [type]="type" (onAuthed)="auth($event)" *ngIf="!authed">
</app-two-factor-verify>
<ng-container *ngIf="authed">
<div class="modal-body text-center">
<ng-container *ngIf="code">

View File

@ -1,5 +1,5 @@
import { Component, OnDestroy, OnInit, Type, ViewChild, ViewContainerRef } from "@angular/core";
import { firstValueFrom, Observable, Subject, takeUntil } from "rxjs";
import { firstValueFrom, lastValueFrom, Observable, Subject, takeUntil } from "rxjs";
import { ModalRef } from "@bitwarden/angular/components/modal/modal.ref";
import { ModalService } from "@bitwarden/angular/services/modal.service";
@ -8,15 +8,23 @@ import { PolicyService } from "@bitwarden/common/admin-console/abstractions/poli
import { PolicyType } from "@bitwarden/common/admin-console/enums";
import { Organization } from "@bitwarden/common/admin-console/models/domain/organization";
import { TwoFactorProviderType } from "@bitwarden/common/auth/enums/two-factor-provider-type";
import { TwoFactorAuthenticatorResponse } from "@bitwarden/common/auth/models/response/two-factor-authenticator.response";
import { TwoFactorDuoResponse } from "@bitwarden/common/auth/models/response/two-factor-duo.response";
import { TwoFactorEmailResponse } from "@bitwarden/common/auth/models/response/two-factor-email.response";
import { TwoFactorWebAuthnResponse } from "@bitwarden/common/auth/models/response/two-factor-web-authn.response";
import { TwoFactorYubiKeyResponse } from "@bitwarden/common/auth/models/response/two-factor-yubi-key.response";
import { TwoFactorProviders } from "@bitwarden/common/auth/services/two-factor.service";
import { AuthResponse } from "@bitwarden/common/auth/types/auth-response";
import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions/account/billing-account-profile-state.service";
import { ProductType } from "@bitwarden/common/enums";
import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service";
import { DialogService } from "@bitwarden/components";
import { TwoFactorAuthenticatorComponent } from "./two-factor-authenticator.component";
import { TwoFactorDuoComponent } from "./two-factor-duo.component";
import { TwoFactorEmailComponent } from "./two-factor-email.component";
import { TwoFactorRecoveryComponent } from "./two-factor-recovery.component";
import { TwoFactorVerifyComponent } from "./two-factor-verify.component";
import { TwoFactorWebAuthnComponent } from "./two-factor-webauthn.component";
import { TwoFactorYubiKeyComponent } from "./two-factor-yubikey.component";
@ -52,6 +60,7 @@ export class TwoFactorSetupComponent implements OnInit, OnDestroy {
private twoFactorAuthPolicyAppliesToActiveUser: boolean;
constructor(
protected dialogService: DialogService,
protected apiService: ApiService,
protected modalService: ModalService,
protected messagingService: MessagingService,
@ -114,50 +123,82 @@ export class TwoFactorSetupComponent implements OnInit, OnDestroy {
this.loading = false;
}
async callTwoFactorVerifyDialog(type?: TwoFactorProviderType) {
const twoFactorVerifyDialogRef = TwoFactorVerifyComponent.open(this.dialogService, {
data: { type: type, organizationId: this.organizationId },
});
return await lastValueFrom(twoFactorVerifyDialogRef.closed);
}
async manage(type: TwoFactorProviderType) {
switch (type) {
case TwoFactorProviderType.Authenticator: {
const result: AuthResponse<TwoFactorAuthenticatorResponse> =
await this.callTwoFactorVerifyDialog(type);
if (!result) {
return;
}
const authComp = await this.openModal(
this.authenticatorModalRef,
TwoFactorAuthenticatorComponent,
);
// eslint-disable-next-line rxjs-angular/prefer-takeuntil
authComp.onUpdated.subscribe((enabled: boolean) => {
await authComp.auth(result);
authComp.onUpdated.pipe(takeUntil(this.destroy$)).subscribe((enabled: boolean) => {
this.updateStatus(enabled, TwoFactorProviderType.Authenticator);
});
break;
}
case TwoFactorProviderType.Yubikey: {
const result: AuthResponse<TwoFactorYubiKeyResponse> =
await this.callTwoFactorVerifyDialog(type);
if (!result) {
return;
}
const yubiComp = await this.openModal(this.yubikeyModalRef, TwoFactorYubiKeyComponent);
// eslint-disable-next-line rxjs-angular/prefer-takeuntil
yubiComp.onUpdated.subscribe((enabled: boolean) => {
yubiComp.auth(result);
yubiComp.onUpdated.pipe(takeUntil(this.destroy$)).subscribe((enabled: boolean) => {
this.updateStatus(enabled, TwoFactorProviderType.Yubikey);
});
break;
}
case TwoFactorProviderType.Duo: {
const result: AuthResponse<TwoFactorDuoResponse> =
await this.callTwoFactorVerifyDialog(type);
if (!result) {
return;
}
const duoComp = await this.openModal(this.duoModalRef, TwoFactorDuoComponent);
// eslint-disable-next-line rxjs-angular/prefer-takeuntil
duoComp.onUpdated.subscribe((enabled: boolean) => {
duoComp.auth(result);
duoComp.onUpdated.pipe(takeUntil(this.destroy$)).subscribe((enabled: boolean) => {
this.updateStatus(enabled, TwoFactorProviderType.Duo);
});
break;
}
case TwoFactorProviderType.Email: {
const result: AuthResponse<TwoFactorEmailResponse> =
await this.callTwoFactorVerifyDialog(type);
if (!result) {
return;
}
const emailComp = await this.openModal(this.emailModalRef, TwoFactorEmailComponent);
// eslint-disable-next-line rxjs-angular/prefer-takeuntil
emailComp.onUpdated.subscribe((enabled: boolean) => {
await emailComp.auth(result);
emailComp.onUpdated.pipe(takeUntil(this.destroy$)).subscribe((enabled: boolean) => {
this.updateStatus(enabled, TwoFactorProviderType.Email);
});
break;
}
case TwoFactorProviderType.WebAuthn: {
const result: AuthResponse<TwoFactorWebAuthnResponse> =
await this.callTwoFactorVerifyDialog(type);
if (!result) {
return;
}
const webAuthnComp = await this.openModal(
this.webAuthnModalRef,
TwoFactorWebAuthnComponent,
);
// eslint-disable-next-line rxjs-angular/prefer-takeuntil
webAuthnComp.onUpdated.subscribe((enabled: boolean) => {
webAuthnComp.auth(result);
webAuthnComp.onUpdated.pipe(takeUntil(this.destroy$)).subscribe((enabled: boolean) => {
this.updateStatus(enabled, TwoFactorProviderType.WebAuthn);
});
break;
@ -167,10 +208,12 @@ export class TwoFactorSetupComponent implements OnInit, OnDestroy {
}
}
recoveryCode() {
// 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.openModal(this.recoveryModalRef, TwoFactorRecoveryComponent);
async recoveryCode() {
const result = await this.callTwoFactorVerifyDialog(-1 as TwoFactorProviderType);
if (result) {
const recoverComp = await this.openModal(this.recoveryModalRef, TwoFactorRecoveryComponent);
recoverComp.auth(result);
}
}
async premiumRequired() {

View File

@ -1,15 +1,23 @@
<form #form (ngSubmit)="submit()" [appApiAction]="formPromise" ngNativeValidate>
<div class="modal-body">
<app-user-verification [(ngModel)]="secret" ngDefaultControl name="secret">
</app-user-verification>
</div>
<div class="modal-footer">
<button type="submit" class="btn btn-primary btn-submit" [disabled]="form.loading">
<i class="bwi bwi-spinner bwi-spin" title="{{ 'loading' | i18n }}" aria-hidden="true"></i>
<span>{{ "continue" | i18n }}</span>
</button>
<button type="button" class="btn btn-outline-secondary" data-dismiss="modal">
{{ "close" | i18n }}
</button>
</div>
<form [formGroup]="formGroup" [bitSubmit]="submit">
<bit-dialog dialogSize="default">
<span bitDialogTitle>
{{ "twoStepLogin" | i18n }}
<small class="tw-text-muted">{{ dialogTitle }}</small>
</span>
<ng-container bitDialogContent>
<app-user-verification-form-input
formControlName="secret"
ngDefaultControl
name="secret"
></app-user-verification-form-input>
</ng-container>
<ng-container bitDialogFooter>
<button bitButton bitFormButton type="submit" buttonType="primary">
{{ "continue" | i18n }}
</button>
<button bitButton type="button" buttonType="secondary" bitDialogClose>
{{ "close" | i18n }}
</button>
</ng-container>
</bit-dialog>
</form>

View File

@ -1,4 +1,6 @@
import { Component, EventEmitter, Input, Output } from "@angular/core";
import { DIALOG_DATA, DialogConfig, DialogRef } from "@angular/cdk/dialog";
import { Component, EventEmitter, Inject, Output } from "@angular/core";
import { FormControl, FormGroup } from "@angular/forms";
import { ApiService } from "@bitwarden/common/abstractions/api.service";
import { UserVerificationService } from "@bitwarden/common/auth/abstractions/user-verification/user-verification.service.abstraction";
@ -8,46 +10,74 @@ import { SecretVerificationRequest } from "@bitwarden/common/auth/models/request
import { AuthResponse } from "@bitwarden/common/auth/types/auth-response";
import { TwoFactorResponse } from "@bitwarden/common/auth/types/two-factor-response";
import { Verification } from "@bitwarden/common/auth/types/verification";
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { DialogService } from "@bitwarden/components";
type TwoFactorVerifyDialogData = {
type: TwoFactorProviderType;
organizationId: string;
};
@Component({
selector: "app-two-factor-verify",
templateUrl: "two-factor-verify.component.html",
})
export class TwoFactorVerifyComponent {
@Input() type: TwoFactorProviderType;
@Input() organizationId: string;
type: TwoFactorProviderType;
organizationId: string;
@Output() onAuthed = new EventEmitter<AuthResponse<TwoFactorResponse>>();
secret: Verification;
formPromise: Promise<TwoFactorResponse>;
protected formGroup = new FormGroup({
secret: new FormControl<Verification | null>(null),
});
constructor(
@Inject(DIALOG_DATA) protected data: TwoFactorVerifyDialogData,
private dialogRef: DialogRef,
private apiService: ApiService,
private logService: LogService,
private i18nService: I18nService,
private userVerificationService: UserVerificationService,
) {}
) {
this.type = data.type;
this.organizationId = data.organizationId;
}
async submit() {
submit = async () => {
let hashedSecret: string;
try {
this.formPromise = this.userVerificationService.buildRequest(this.secret).then((request) => {
this.formPromise = this.userVerificationService
.buildRequest(this.formGroup.value.secret)
.then((request) => {
hashedSecret =
this.secret.type === VerificationType.MasterPassword
this.formGroup.value.secret.type === VerificationType.MasterPassword
? request.masterPasswordHash
: request.otp;
return this.apiCall(request);
});
const response = await this.formPromise;
this.onAuthed.emit({
response: response,
secret: hashedSecret,
verificationType: this.secret.type,
});
} catch (e) {
this.logService.error(e);
const response = await this.formPromise;
this.dialogRef.close({
response: response,
secret: hashedSecret,
verificationType: this.formGroup.value.secret.type,
});
};
get dialogTitle(): string {
switch (this.type) {
case -1 as TwoFactorProviderType:
return this.i18nService.t("recoveryCodeTitle");
case TwoFactorProviderType.Duo:
return "Duo";
case TwoFactorProviderType.Email:
return this.i18nService.t("emailTitle");
case TwoFactorProviderType.WebAuthn:
return this.i18nService.t("webAuthnTitle");
case TwoFactorProviderType.Authenticator:
return this.i18nService.t("authenticatorAppTitle");
case TwoFactorProviderType.Yubikey:
return "Yubikey";
}
}
@ -72,4 +102,8 @@ export class TwoFactorVerifyComponent {
return this.apiService.getTwoFactorYubiKey(request);
}
}
static open(dialogService: DialogService, config: DialogConfig<TwoFactorVerifyDialogData>) {
return dialogService.open<AuthResponse<any>>(TwoFactorVerifyComponent, config);
}
}

View File

@ -15,13 +15,6 @@
<span aria-hidden="true">&times;</span>
</button>
</div>
<app-two-factor-verify
[organizationId]="organizationId"
[type]="type"
(onAuthed)="auth($any($event))"
*ngIf="!authed"
>
</app-two-factor-verify>
<form
#form
(ngSubmit)="submit()"

View File

@ -15,13 +15,6 @@
<span aria-hidden="true">&times;</span>
</button>
</div>
<app-two-factor-verify
[organizationId]="organizationId"
[type]="type"
(onAuthed)="auth($any($event))"
*ngIf="!authed"
>
</app-two-factor-verify>
<form
#form
(ngSubmit)="submit()"

View File

@ -4,6 +4,7 @@ import { Router } from "@angular/router";
import { UpdatePasswordComponent as BaseUpdatePasswordComponent } from "@bitwarden/angular/auth/components/update-password.component";
import { ApiService } from "@bitwarden/common/abstractions/api.service";
import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction";
import { KdfConfigService } from "@bitwarden/common/auth/abstractions/kdf-config.service";
import { UserVerificationService } from "@bitwarden/common/auth/abstractions/user-verification/user-verification.service.abstraction";
import { CryptoService } from "@bitwarden/common/platform/abstractions/crypto.service";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
@ -32,6 +33,7 @@ export class UpdatePasswordComponent extends BaseUpdatePasswordComponent {
stateService: StateService,
userVerificationService: UserVerificationService,
dialogService: DialogService,
kdfConfigService: KdfConfigService,
) {
super(
router,
@ -46,6 +48,7 @@ export class UpdatePasswordComponent extends BaseUpdatePasswordComponent {
userVerificationService,
logService,
dialogService,
kdfConfigService,
);
}
}

View File

@ -84,7 +84,6 @@ export class OrganizationSubscriptionCloudComponent implements OnInit, OnDestroy
this.showUpdatedSubscriptionStatusSection$ = this.configService.getFeatureFlag$(
FeatureFlag.AC1795_UpdatedSubscriptionStatusSection,
false,
);
}

View File

@ -13,25 +13,27 @@
</span>
</ng-container>
</bit-nav-item>
<section
*ngIf="((moreProducts$ | async) ?? []).length > 0"
class="tw-mt-2 tw-flex tw-w-full tw-flex-col tw-gap-2 tw-border-0 tw-border-t tw-border-solid tw-border-t-text-alt2"
>
<span class="tw-text-xs !tw-text-alt2 tw-p-2 tw-pb-0">{{ "moreFromBitwarden" | i18n }}</span>
<a
*ngFor="let more of moreProducts$ | async"
[href]="more.marketingRoute"
target="_blank"
rel="noreferrer"
class="tw-flex tw-py-2 tw-px-4 tw-font-semibold !tw-text-alt2 !tw-no-underline hover:tw-bg-primary-300/60 [&>:not(.bwi)]:hover:tw-underline"
<ng-container *ngIf="(moreProducts$ | async) ?? [] as moreProducts">
<section
*ngIf="moreProducts.length > 0"
class="tw-mt-2 tw-flex tw-w-full tw-flex-col tw-gap-2 tw-border-0 tw-border-t tw-border-solid tw-border-t-text-alt2"
>
<i class="bwi bwi-fw {{ more.icon }} tw-mt-1"></i>
<div>
{{ more.navigationUIDetails?.name ?? more.name }}
<div *ngIf="more.navigationUIDetails?.supportingText" class="tw-text-xs tw-font-normal">
{{ more.navigationUIDetails.supportingText }}
<span class="tw-text-xs !tw-text-alt2 tw-p-2 tw-pb-0">{{ "moreFromBitwarden" | i18n }}</span>
<a
*ngFor="let more of moreProducts"
[href]="more.marketingRoute"
target="_blank"
rel="noreferrer"
class="tw-flex tw-py-2 tw-px-4 tw-font-semibold !tw-text-alt2 !tw-no-underline hover:tw-bg-primary-300/60 [&>:not(.bwi)]:hover:tw-underline"
>
<i class="bwi bwi-fw {{ more.icon }} tw-mt-1"></i>
<div>
{{ more.otherProductOverrides?.name ?? more.name }}
<div *ngIf="more.otherProductOverrides?.supportingText" class="tw-text-xs tw-font-normal">
{{ more.otherProductOverrides.supportingText }}
</div>
</div>
</div>
</a>
</section>
</a>
</section>
</ng-container>
</div>

View File

@ -77,7 +77,7 @@ describe("NavigationProductSwitcherComponent", () => {
expect(link.getAttribute("href")).toBe("https://www.example.com/");
});
it("uses `navigationUIDetails` when available", () => {
it("uses `otherProductOverrides` when available", () => {
mockProducts$.next({
bento: [],
other: [
@ -86,7 +86,7 @@ describe("NavigationProductSwitcherComponent", () => {
name: "Other Product",
icon: "bwi-lock",
marketingRoute: "https://www.example.com/",
navigationUIDetails: { name: "Alternate name" },
otherProductOverrides: { name: "Alternate name" },
},
],
});
@ -103,7 +103,7 @@ describe("NavigationProductSwitcherComponent", () => {
name: "Other Product",
icon: "bwi-lock",
marketingRoute: "https://www.example.com/",
navigationUIDetails: { name: "Alternate name", supportingText: "Supporting Text" },
otherProductOverrides: { name: "Alternate name", supportingText: "Supporting Text" },
},
],
});

View File

@ -1,4 +1,4 @@
import { Component, HostBinding } from "@angular/core";
import { Component } from "@angular/core";
import { map, Observable } from "rxjs";
import { ProductSwitcherItem, ProductSwitcherService } from "../shared/product-switcher.service";
@ -8,17 +8,15 @@ import { ProductSwitcherItem, ProductSwitcherService } from "../shared/product-s
templateUrl: "./navigation-switcher.component.html",
})
export class NavigationProductSwitcherComponent {
// Use margin-top: auto to push the component to the bottom of the parent flex container
@HostBinding("style.margin-top") marginTop = "auto";
constructor(private productSwitcherService: ProductSwitcherService) {}
accessibleProducts$: Observable<ProductSwitcherItem[]> =
protected readonly accessibleProducts$: Observable<ProductSwitcherItem[]> =
this.productSwitcherService.products$.pipe(
map((products) => (products.bento ?? []).filter((item) => !item.isActive)),
);
moreProducts$: Observable<ProductSwitcherItem[]> = this.productSwitcherService.products$.pipe(
map((products) => (products.other ?? []).filter((item) => !item.isActive)),
);
protected readonly moreProducts$: Observable<ProductSwitcherItem[]> =
this.productSwitcherService.products$.pipe(
map((products) => (products.other ?? []).filter((item) => !item.isActive)),
);
}

View File

@ -1,6 +1,6 @@
import { Component, Directive, importProvidersFrom, Input } from "@angular/core";
import { RouterModule } from "@angular/router";
import { applicationConfig, Meta, moduleMetadata, StoryFn } from "@storybook/angular";
import { applicationConfig, Meta, moduleMetadata, StoryObj } from "@storybook/angular";
import { BehaviorSubject, firstValueFrom } from "rxjs";
import { I18nPipe } from "@bitwarden/angular/platform/pipes/i18n.pipe";
@ -115,40 +115,58 @@ export default {
],
}),
],
} as Meta;
} as Meta<NavigationProductSwitcherComponent>;
const Template: StoryFn = (args) => ({
props: args,
template: `
type Story = StoryObj<
NavigationProductSwitcherComponent & MockProviderService & MockOrganizationService
>;
const Template: Story = {
render: (args) => ({
props: args,
template: `
<router-outlet [mockOrgs]="mockOrgs" [mockProviders]="mockProviders"></router-outlet>
<bit-layout>
<nav slot="sidebar" class="tw-flex tw-flex-col tw-h-full">
<navigation-product-switcher></navigation-product-switcher>
</nav>
</bit-layout>
`,
});
export const OnlyPM = Template.bind({});
OnlyPM.args = {
mockOrgs: [],
mockProviders: [],
<div class="tw-bg-background-alt3 tw-w-60">
<navigation-product-switcher></navigation-product-switcher>
</div>
`,
}),
};
export const SMAvailable = Template.bind({});
SMAvailable.args = {
mockOrgs: [{ id: "org-a", canManageUsers: false, canAccessSecretsManager: true, enabled: true }],
mockProviders: [],
export const OnlyPM: Story = {
...Template,
args: {
mockOrgs: [],
mockProviders: [],
},
};
export const SMAndACAvailable = Template.bind({});
SMAndACAvailable.args = {
mockOrgs: [{ id: "org-a", canManageUsers: true, canAccessSecretsManager: true, enabled: true }],
mockProviders: [],
export const SMAvailable: Story = {
...Template,
args: {
mockOrgs: [
{ id: "org-a", canManageUsers: false, canAccessSecretsManager: true, enabled: true },
] as Organization[],
mockProviders: [],
},
};
export const WithAllOptions = Template.bind({});
WithAllOptions.args = {
mockOrgs: [{ id: "org-a", canManageUsers: true, canAccessSecretsManager: true, enabled: true }],
mockProviders: [{ id: "provider-a" }],
export const SMAndACAvailable: Story = {
...Template,
args: {
mockOrgs: [
{ id: "org-a", canManageUsers: true, canAccessSecretsManager: true, enabled: true },
] as Organization[],
mockProviders: [],
},
};
export const WithAllOptions: Story = {
...Template,
args: {
mockOrgs: [
{ id: "org-a", canManageUsers: true, canAccessSecretsManager: true, enabled: true },
] as Organization[],
mockProviders: [{ id: "provider-a" }] as Provider[],
},
};

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