diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index ae5d62a90c2..f03cf3ee2a8 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -8,6 +8,7 @@ apps/desktop/desktop_native @bitwarden/team-platform-dev apps/desktop/desktop_native/objc/src/native/autofill @bitwarden/team-autofill-desktop-dev apps/desktop/desktop_native/core/src/autofill @bitwarden/team-autofill-desktop-dev +apps/desktop/desktop_native/core/src/secure_memory @bitwarden/team-key-management-dev ## No ownership for Cargo.lock and Cargo.toml to allow dependency updates apps/desktop/desktop_native/Cargo.lock apps/desktop/desktop_native/Cargo.toml diff --git a/.github/renovate.json5 b/.github/renovate.json5 index 6fcffb1f875..f898df460c9 100644 --- a/.github/renovate.json5 +++ b/.github/renovate.json5 @@ -400,7 +400,16 @@ reviewers: ["team:team-vault-dev"], }, { - matchPackageNames: ["aes", "big-integer", "cbc", "rsa", "russh-cryptovec", "sha2"], + matchPackageNames: [ + "aes", + "big-integer", + "cbc", + "rsa", + "russh-cryptovec", + "sha2", + "memsec", + "linux-keyutils", + ], description: "Key Management owned dependencies", commitMessagePrefix: "[deps] KM:", reviewers: ["team:team-key-management-dev"], diff --git a/.gitignore b/.gitignore index 61a20195592..6b13d22caa7 100644 --- a/.gitignore +++ b/.gitignore @@ -28,6 +28,7 @@ npm-debug.log # Build directories dist build +target .angular/cache .flatpak .flatpak-repo diff --git a/apps/browser/src/auth/popup/account-switching/account-switcher.component.ts b/apps/browser/src/auth/popup/account-switching/account-switcher.component.ts index 48fd57431a2..9e9a1ecf570 100644 --- a/apps/browser/src/auth/popup/account-switching/account-switcher.component.ts +++ b/apps/browser/src/auth/popup/account-switching/account-switcher.component.ts @@ -33,6 +33,8 @@ import { AccountComponent } from "./account.component"; import { CurrentAccountComponent } from "./current-account.component"; import { AccountSwitcherService } from "./services/account-switcher.service"; +// FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush +// eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection @Component({ templateUrl: "account-switcher.component.html", imports: [ diff --git a/apps/browser/src/auth/popup/account-switching/account.component.ts b/apps/browser/src/auth/popup/account-switching/account.component.ts index c060d9161ef..edfad2a54b3 100644 --- a/apps/browser/src/auth/popup/account-switching/account.component.ts +++ b/apps/browser/src/auth/popup/account-switching/account.component.ts @@ -13,13 +13,19 @@ import { BiometricsService } from "@bitwarden/key-management"; import { AccountSwitcherService, AvailableAccount } from "./services/account-switcher.service"; +// FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush +// eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection @Component({ selector: "auth-account", templateUrl: "account.component.html", imports: [CommonModule, JslibModule, AvatarModule, ItemModule], }) export class AccountComponent { + // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals + // eslint-disable-next-line @angular-eslint/prefer-signals @Input() account: AvailableAccount; + // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals + // eslint-disable-next-line @angular-eslint/prefer-output-emitter-ref @Output() loading = new EventEmitter(); constructor( diff --git a/apps/browser/src/auth/popup/account-switching/current-account.component.ts b/apps/browser/src/auth/popup/account-switching/current-account.component.ts index 63e8481621a..2dde3b5a266 100644 --- a/apps/browser/src/auth/popup/account-switching/current-account.component.ts +++ b/apps/browser/src/auth/popup/account-switching/current-account.component.ts @@ -21,6 +21,8 @@ export type CurrentAccount = { avatarColor: string; }; +// FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush +// eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection @Component({ selector: "app-current-account", templateUrl: "current-account.component.html", diff --git a/apps/browser/src/auth/popup/components/set-pin.component.ts b/apps/browser/src/auth/popup/components/set-pin.component.ts index a9e8e1b122f..dbb71ae3b07 100644 --- a/apps/browser/src/auth/popup/components/set-pin.component.ts +++ b/apps/browser/src/auth/popup/components/set-pin.component.ts @@ -13,6 +13,8 @@ import { IconButtonModule, } from "@bitwarden/components"; +// FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush +// eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection @Component({ templateUrl: "set-pin.component.html", imports: [ diff --git a/apps/browser/src/auth/popup/settings/account-security.component.spec.ts b/apps/browser/src/auth/popup/settings/account-security.component.spec.ts index 2335c5c2e69..aa3639e9e93 100644 --- a/apps/browser/src/auth/popup/settings/account-security.component.spec.ts +++ b/apps/browser/src/auth/popup/settings/account-security.component.spec.ts @@ -41,6 +41,8 @@ import { PopupRouterCacheService } from "../../../platform/popup/view-cache/popu import { AccountSecurityComponent } from "./account-security.component"; +// FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush +// eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection @Component({ selector: "app-pop-out", template: ` `, diff --git a/apps/browser/src/auth/popup/settings/account-security.component.ts b/apps/browser/src/auth/popup/settings/account-security.component.ts index 4eb24d19605..65a0d33f93e 100644 --- a/apps/browser/src/auth/popup/settings/account-security.component.ts +++ b/apps/browser/src/auth/popup/settings/account-security.component.ts @@ -78,6 +78,8 @@ import { SetPinComponent } from "../components/set-pin.component"; import { AwaitDesktopDialogComponent } from "./await-desktop-dialog.component"; +// FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush +// eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection @Component({ templateUrl: "account-security.component.html", imports: [ diff --git a/apps/browser/src/auth/popup/settings/await-desktop-dialog.component.ts b/apps/browser/src/auth/popup/settings/await-desktop-dialog.component.ts index 11bb9683bb9..a64cea1ef3e 100644 --- a/apps/browser/src/auth/popup/settings/await-desktop-dialog.component.ts +++ b/apps/browser/src/auth/popup/settings/await-desktop-dialog.component.ts @@ -3,6 +3,8 @@ import { Component } from "@angular/core"; import { JslibModule } from "@bitwarden/angular/jslib.module"; import { ButtonModule, DialogModule, DialogService } from "@bitwarden/components"; +// FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush +// eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection @Component({ templateUrl: "await-desktop-dialog.component.html", imports: [JslibModule, ButtonModule, DialogModule], diff --git a/apps/browser/src/auth/popup/settings/extension-device-management.component.ts b/apps/browser/src/auth/popup/settings/extension-device-management.component.ts index 793965db141..b431fc874dd 100644 --- a/apps/browser/src/auth/popup/settings/extension-device-management.component.ts +++ b/apps/browser/src/auth/popup/settings/extension-device-management.component.ts @@ -7,6 +7,8 @@ import { PopOutComponent } from "../../../platform/popup/components/pop-out.comp import { PopupHeaderComponent } from "../../../platform/popup/layout/popup-header.component"; import { PopupPageComponent } from "../../../platform/popup/layout/popup-page.component"; +// FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush +// eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection @Component({ standalone: true, selector: "extension-device-management", diff --git a/apps/browser/src/autofill/popup/fido2/fido2-cipher-row.component.ts b/apps/browser/src/autofill/popup/fido2/fido2-cipher-row.component.ts index 074e23d642d..adabae2c31d 100644 --- a/apps/browser/src/autofill/popup/fido2/fido2-cipher-row.component.ts +++ b/apps/browser/src/autofill/popup/fido2/fido2-cipher-row.component.ts @@ -28,9 +28,17 @@ import { ], }) export class Fido2CipherRowComponent { + // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals + // eslint-disable-next-line @angular-eslint/prefer-output-emitter-ref @Output() onSelected = new EventEmitter(); + // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals + // eslint-disable-next-line @angular-eslint/prefer-signals @Input() cipher: CipherView; + // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals + // eslint-disable-next-line @angular-eslint/prefer-signals @Input() last: boolean; + // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals + // eslint-disable-next-line @angular-eslint/prefer-signals @Input() title: string; protected selectCipher(c: CipherView) { diff --git a/apps/browser/src/autofill/popup/fido2/fido2-use-browser-link.component.ts b/apps/browser/src/autofill/popup/fido2/fido2-use-browser-link.component.ts index b8b49f993e3..f4c4c871478 100644 --- a/apps/browser/src/autofill/popup/fido2/fido2-use-browser-link.component.ts +++ b/apps/browser/src/autofill/popup/fido2/fido2-use-browser-link.component.ts @@ -15,6 +15,8 @@ import { MenuModule } from "@bitwarden/components"; import { fido2PopoutSessionData$ } from "../../../vault/popup/utils/fido2-popout-session-data"; import { BrowserFido2UserInterfaceSession } from "../../fido2/services/browser-fido2-user-interface.service"; +// FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush +// eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection @Component({ selector: "app-fido2-use-browser-link", templateUrl: "fido2-use-browser-link.component.html", diff --git a/apps/browser/src/autofill/popup/fido2/fido2.component.ts b/apps/browser/src/autofill/popup/fido2/fido2.component.ts index 11e00749bdf..c6799f93a5e 100644 --- a/apps/browser/src/autofill/popup/fido2/fido2.component.ts +++ b/apps/browser/src/autofill/popup/fido2/fido2.component.ts @@ -71,6 +71,8 @@ interface ViewData { fallbackSupported: boolean; } +// FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush +// eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection @Component({ selector: "app-fido2", templateUrl: "fido2.component.html", diff --git a/apps/browser/src/autofill/popup/settings/autofill.component.ts b/apps/browser/src/autofill/popup/settings/autofill.component.ts index c3b5915a10a..62e5ba3a151 100644 --- a/apps/browser/src/autofill/popup/settings/autofill.component.ts +++ b/apps/browser/src/autofill/popup/settings/autofill.component.ts @@ -77,6 +77,8 @@ import { PopOutComponent } from "../../../platform/popup/components/pop-out.comp import { PopupHeaderComponent } from "../../../platform/popup/layout/popup-header.component"; import { PopupPageComponent } from "../../../platform/popup/layout/popup-page.component"; +// FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush +// eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection @Component({ templateUrl: "autofill.component.html", imports: [ diff --git a/apps/browser/src/autofill/popup/settings/blocked-domains.component.ts b/apps/browser/src/autofill/popup/settings/blocked-domains.component.ts index 15379eff436..30a64e03c56 100644 --- a/apps/browser/src/autofill/popup/settings/blocked-domains.component.ts +++ b/apps/browser/src/autofill/popup/settings/blocked-domains.component.ts @@ -41,6 +41,8 @@ import { PopupHeaderComponent } from "../../../platform/popup/layout/popup-heade import { PopupPageComponent } from "../../../platform/popup/layout/popup-page.component"; import { PopupRouterCacheService } from "../../../platform/popup/view-cache/popup-router-cache.service"; +// FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush +// eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection @Component({ selector: "app-blocked-domains", templateUrl: "blocked-domains.component.html", @@ -66,6 +68,8 @@ import { PopupRouterCacheService } from "../../../platform/popup/view-cache/popu ], }) export class BlockedDomainsComponent implements AfterViewInit, OnDestroy { + // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals + // eslint-disable-next-line @angular-eslint/prefer-signals @ViewChildren("uriInput") uriInputElements: QueryList> = new QueryList(); diff --git a/apps/browser/src/autofill/popup/settings/excluded-domains.component.ts b/apps/browser/src/autofill/popup/settings/excluded-domains.component.ts index a5bfad726f5..e67c826cac6 100644 --- a/apps/browser/src/autofill/popup/settings/excluded-domains.component.ts +++ b/apps/browser/src/autofill/popup/settings/excluded-domains.component.ts @@ -42,6 +42,8 @@ import { PopupHeaderComponent } from "../../../platform/popup/layout/popup-heade import { PopupPageComponent } from "../../../platform/popup/layout/popup-page.component"; import { PopupRouterCacheService } from "../../../platform/popup/view-cache/popup-router-cache.service"; +// FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush +// eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection @Component({ selector: "app-excluded-domains", templateUrl: "excluded-domains.component.html", @@ -67,6 +69,8 @@ import { PopupRouterCacheService } from "../../../platform/popup/view-cache/popu ], }) export class ExcludedDomainsComponent implements AfterViewInit, OnDestroy { + // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals + // eslint-disable-next-line @angular-eslint/prefer-signals @ViewChildren("uriInput") uriInputElements: QueryList> = new QueryList(); diff --git a/apps/browser/src/autofill/popup/settings/notifications.component.ts b/apps/browser/src/autofill/popup/settings/notifications.component.ts index cb10dec620b..3c77d746e9c 100644 --- a/apps/browser/src/autofill/popup/settings/notifications.component.ts +++ b/apps/browser/src/autofill/popup/settings/notifications.component.ts @@ -21,6 +21,8 @@ import { PopOutComponent } from "../../../platform/popup/components/pop-out.comp import { PopupHeaderComponent } from "../../../platform/popup/layout/popup-header.component"; import { PopupPageComponent } from "../../../platform/popup/layout/popup-page.component"; +// FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush +// eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection @Component({ templateUrl: "notifications.component.html", imports: [ diff --git a/apps/browser/src/background/main.background.ts b/apps/browser/src/background/main.background.ts index 7a4ee64070f..8170c2a65a0 100644 --- a/apps/browser/src/background/main.background.ts +++ b/apps/browser/src/background/main.background.ts @@ -1050,6 +1050,7 @@ export default class MainBackground { this.authService, this.stateProvider, this.securityStateService, + this.kdfConfigService, ); this.syncServiceListener = new SyncServiceListener( diff --git a/apps/browser/src/vault/popup/components/vault-v2/item-more-options/item-more-options.component.ts b/apps/browser/src/vault/popup/components/vault-v2/item-more-options/item-more-options.component.ts index 83535b09e66..1b8403e6024 100644 --- a/apps/browser/src/vault/popup/components/vault-v2/item-more-options/item-more-options.component.ts +++ b/apps/browser/src/vault/popup/components/vault-v2/item-more-options/item-more-options.component.ts @@ -267,6 +267,11 @@ export class ItemMoreOptionsComponent { } protected async delete() { + const repromptPassed = await this.passwordRepromptService.passwordRepromptCheck(this.cipher); + if (!repromptPassed) { + return; + } + const confirmed = await this.dialogService.openSimpleDialog({ title: { key: "deleteItem" }, content: { key: "deleteItemConfirmation" }, diff --git a/apps/cli/src/service-container/service-container.ts b/apps/cli/src/service-container/service-container.ts index d10849bae0f..3c4ee55361f 100644 --- a/apps/cli/src/service-container/service-container.ts +++ b/apps/cli/src/service-container/service-container.ts @@ -846,6 +846,7 @@ export class ServiceContainer { this.authService, this.stateProvider, this.securityStateService, + this.kdfConfigService, ); this.totpService = new TotpService(this.sdkService); diff --git a/apps/desktop/desktop_native/Cargo.lock b/apps/desktop/desktop_native/Cargo.lock index c47f2db22e5..f6391350d14 100644 --- a/apps/desktop/desktop_native/Cargo.lock +++ b/apps/desktop/desktop_native/Cargo.lock @@ -941,6 +941,8 @@ dependencies = [ "interprocess", "keytar", "libc", + "linux-keyutils", + "memsec", "oo7", "pin-project", "pkcs8", @@ -1807,6 +1809,16 @@ dependencies = [ "cc", ] +[[package]] +name = "linux-keyutils" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "761e49ec5fd8a5a463f9b84e877c373d888935b71c6be78f3767fe2ae6bed18e" +dependencies = [ + "bitflags", + "libc", +] + [[package]] name = "linux-raw-sys" version = "0.4.15" @@ -1892,6 +1904,17 @@ dependencies = [ "autocfg", ] +[[package]] +name = "memsec" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c797b9d6bb23aab2fc369c65f871be49214f5c759af65bde26ffaaa2b646b492" +dependencies = [ + "getrandom 0.2.16", + "libc", + "windows-sys 0.45.0", +] + [[package]] name = "mime" version = "0.3.17" @@ -4090,6 +4113,15 @@ dependencies = [ "windows-link 0.1.3", ] +[[package]] +name = "windows-sys" +version = "0.45.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75283be5efb2831d37ea142365f009c02ec203cd29a3ebecbc093d52315b66d0" +dependencies = [ + "windows-targets 0.42.2", +] + [[package]] name = "windows-sys" version = "0.52.0" @@ -4126,6 +4158,21 @@ dependencies = [ "windows-link 0.2.0", ] +[[package]] +name = "windows-targets" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e5180c00cd44c9b1c88adb3693291f1cd93605ded80c250a75d472756b4d071" +dependencies = [ + "windows_aarch64_gnullvm 0.42.2", + "windows_aarch64_msvc 0.42.2", + "windows_i686_gnu 0.42.2", + "windows_i686_msvc 0.42.2", + "windows_x86_64_gnu 0.42.2", + "windows_x86_64_gnullvm 0.42.2", + "windows_x86_64_msvc 0.42.2", +] + [[package]] name = "windows-targets" version = "0.48.5" @@ -4174,6 +4221,12 @@ dependencies = [ "windows_x86_64_msvc 0.53.0", ] +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "597a5118570b68bc08d8d59125332c54f1ba9d9adeedeef5b99b02ba2b0698f8" + [[package]] name = "windows_aarch64_gnullvm" version = "0.48.5" @@ -4192,6 +4245,12 @@ version = "0.53.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "86b8d5f90ddd19cb4a147a5fa63ca848db3df085e25fee3cc10b39b6eebae764" +[[package]] +name = "windows_aarch64_msvc" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e08e8864a60f06ef0d0ff4ba04124db8b0fb3be5776a5cd47641e942e58c4d43" + [[package]] name = "windows_aarch64_msvc" version = "0.48.5" @@ -4210,6 +4269,12 @@ version = "0.53.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c7651a1f62a11b8cbd5e0d42526e55f2c99886c77e007179efff86c2b137e66c" +[[package]] +name = "windows_i686_gnu" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c61d927d8da41da96a81f029489353e68739737d3beca43145c8afec9a31a84f" + [[package]] name = "windows_i686_gnu" version = "0.48.5" @@ -4240,6 +4305,12 @@ version = "0.53.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9ce6ccbdedbf6d6354471319e781c0dfef054c81fbc7cf83f338a4296c0cae11" +[[package]] +name = "windows_i686_msvc" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44d840b6ec649f480a41c8d80f9c65108b92d89345dd94027bfe06ac444d1060" + [[package]] name = "windows_i686_msvc" version = "0.48.5" @@ -4267,6 +4338,12 @@ dependencies = [ "windows-core 0.61.0", ] +[[package]] +name = "windows_x86_64_gnu" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8de912b8b8feb55c064867cf047dda097f92d51efad5b491dfb98f6bbb70cb36" + [[package]] name = "windows_x86_64_gnu" version = "0.48.5" @@ -4285,6 +4362,12 @@ version = "0.53.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2e55b5ac9ea33f2fc1716d1742db15574fd6fc8dadc51caab1c16a3d3b4190ba" +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26d41b46a36d453748aedef1486d5c7a85db22e56aff34643984ea85514e94a3" + [[package]] name = "windows_x86_64_gnullvm" version = "0.48.5" @@ -4303,6 +4386,12 @@ version = "0.53.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0a6e035dd0599267ce1ee132e51c27dd29437f63325753051e71dd9e42406c57" +[[package]] +name = "windows_x86_64_msvc" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9aec5da331524158c6d1a4ac0ab1541149c0b9505fde06423b02f5ef0106b9f0" + [[package]] name = "windows_x86_64_msvc" version = "0.48.5" @@ -4522,9 +4611,9 @@ dependencies = [ [[package]] name = "zeroize" -version = "1.8.1" +version = "1.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ced3678a2879b30306d323f4542626697a464a97c0a07c9aebf7ebca65cd4dde" +checksum = "b97154e67e32c85465826e8bcc1c59429aaaf107c1e4a9e53c8d8ccd5eff88d0" dependencies = [ "zeroize_derive", ] diff --git a/apps/desktop/desktop_native/Cargo.toml b/apps/desktop/desktop_native/Cargo.toml index 855b0b3aa43..edf3cb44eca 100644 --- a/apps/desktop/desktop_native/Cargo.toml +++ b/apps/desktop/desktop_native/Cargo.toml @@ -39,7 +39,9 @@ homedir = "=0.3.4" interprocess = "=2.2.1" keytar = "=0.1.6" libc = "=0.2.172" +linux-keyutils = "=0.2.4" log = "=0.4.25" +memsec = "=0.7.0" napi = "=2.16.17" napi-build = "=2.2.0" napi-derive = "=2.16.13" diff --git a/apps/desktop/desktop_native/core/Cargo.toml b/apps/desktop/desktop_native/core/Cargo.toml index b7e4c9b7a83..f6c9d669df6 100644 --- a/apps/desktop/desktop_native/core/Cargo.toml +++ b/apps/desktop/desktop_native/core/Cargo.toml @@ -32,6 +32,7 @@ ed25519 = { workspace = true, features = ["pkcs8"] } futures = { workspace = true } homedir = { workspace = true } interprocess = { workspace = true, features = ["tokio"] } +memsec = { workspace = true, features = ["alloc_ext"] } pin-project = { workspace = true } pkcs8 = { workspace = true, features = ["alloc", "encryption", "pem"] } rand = { workspace = true } @@ -87,6 +88,7 @@ desktop_objc = { path = "../objc" } [target.'cfg(target_os = "linux")'.dependencies] oo7 = { workspace = true } libc = { workspace = true } +linux-keyutils = { workspace = true } ashpd = { workspace = true } zbus = { workspace = true, optional = true } diff --git a/apps/desktop/desktop_native/core/src/secure_memory/dpapi.rs b/apps/desktop/desktop_native/core/src/secure_memory/dpapi.rs index ca9b6081d69..3ff8a6d3d83 100644 --- a/apps/desktop/desktop_native/core/src/secure_memory/dpapi.rs +++ b/apps/desktop/desktop_native/core/src/secure_memory/dpapi.rs @@ -54,7 +54,7 @@ impl SecureMemoryStore for DpapiSecretKVStore { self.map.insert(key, padded_data); } - fn get(&self, key: &str) -> Option> { + fn get(&mut self, key: &str) -> Option> { self.map.get(key).map(|data| { // A copy is created, that is then mutated by the DPAPI unprotect function. let mut data = data.clone(); diff --git a/apps/desktop/desktop_native/core/src/secure_memory/encrypted_memory_store.rs b/apps/desktop/desktop_native/core/src/secure_memory/encrypted_memory_store.rs new file mode 100644 index 00000000000..a8952d8f55a --- /dev/null +++ b/apps/desktop/desktop_native/core/src/secure_memory/encrypted_memory_store.rs @@ -0,0 +1,105 @@ +use tracing::error; + +use crate::secure_memory::{ + secure_key::{EncryptedMemory, SecureMemoryEncryptionKey}, + SecureMemoryStore, +}; + +/// An encrypted memory store holds a platform protected symmetric encryption key, and uses it +/// to encrypt all items it stores. The ciphertexts for the items are not specially protected. This +/// allows circumventing length and amount limitations on platform specific secure memory APIs since +/// only a single short item needs to be protected. +/// +/// The key is briefly in process memory during encryption and decryption, in memory that is protected +/// from swapping to disk via mlock, and then zeroed out immediately after use. +#[allow(unused)] +pub(crate) struct EncryptedMemoryStore { + map: std::collections::HashMap, + memory_encryption_key: SecureMemoryEncryptionKey, +} + +impl EncryptedMemoryStore { + #[allow(unused)] + pub(crate) fn new() -> Self { + EncryptedMemoryStore { + map: std::collections::HashMap::new(), + memory_encryption_key: SecureMemoryEncryptionKey::new(), + } + } +} + +impl SecureMemoryStore for EncryptedMemoryStore { + fn put(&mut self, key: String, value: &[u8]) { + let encrypted_value = self.memory_encryption_key.encrypt(value); + self.map.insert(key, encrypted_value); + } + + fn get(&mut self, key: &str) -> Option> { + let encrypted_memory = self.map.get(key); + if let Some(encrypted_memory) = encrypted_memory { + match self.memory_encryption_key.decrypt(encrypted_memory) { + Ok(plaintext) => Some(plaintext), + Err(_) => { + error!("In memory store, decryption failed for key {}. The memory may have been tampered with. re-keying.", key); + self.memory_encryption_key = SecureMemoryEncryptionKey::new(); + self.clear(); + None + } + } + } else { + None + } + } + + fn has(&self, key: &str) -> bool { + self.map.contains_key(key) + } + + fn remove(&mut self, key: &str) { + self.map.remove(key); + } + + fn clear(&mut self) { + self.map.clear(); + } +} + +impl Drop for EncryptedMemoryStore { + fn drop(&mut self) { + self.clear(); + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_secret_kv_store_various_sizes() { + let mut store = EncryptedMemoryStore::new(); + for size in 0..=2048 { + let key = format!("test_key_{}", size); + let value: Vec = (0..size).map(|i| (i % 256) as u8).collect(); + store.put(key.clone(), &value); + assert!(store.has(&key), "Store should have key for size {}", size); + assert_eq!( + store.get(&key), + Some(value), + "Value mismatch for size {}", + size + ); + } + } + + #[test] + fn test_crud() { + let mut store = EncryptedMemoryStore::new(); + let key = "test_key".to_string(); + let value = vec![1, 2, 3, 4, 5]; + store.put(key.clone(), &value); + assert!(store.has(&key)); + assert_eq!(store.get(&key), Some(value)); + store.remove(&key); + assert!(!store.has(&key)); + } +} diff --git a/apps/desktop/desktop_native/core/src/secure_memory/mod.rs b/apps/desktop/desktop_native/core/src/secure_memory/mod.rs index 0cb604e03f2..8695904758e 100644 --- a/apps/desktop/desktop_native/core/src/secure_memory/mod.rs +++ b/apps/desktop/desktop_native/core/src/secure_memory/mod.rs @@ -1,6 +1,9 @@ #[cfg(target_os = "windows")] pub(crate) mod dpapi; +mod encrypted_memory_store; +mod secure_key; + /// The secure memory store provides an ephemeral key-value store for sensitive data. /// Data stored in this store is prevented from being swapped to disk and zeroed out. Additionally, /// platform-specific protections are applied to prevent memory dumps or debugger access from @@ -12,7 +15,9 @@ pub(crate) trait SecureMemoryStore { /// Retrieves a copy of the value associated with the given key from secure memory. /// This copy does not have additional memory protections applied, and should be zeroed when no /// longer needed. - fn get(&self, key: &str) -> Option>; + /// + /// Note: If memory was tampered with, this will re-key the store and return None. + fn get(&mut self, key: &str) -> Option>; /// Checks if a value is stored under the given key. fn has(&self, key: &str) -> bool; /// Removes the value associated with the given key from secure memory. diff --git a/apps/desktop/desktop_native/core/src/secure_memory/secure_key/crypto.rs b/apps/desktop/desktop_native/core/src/secure_memory/secure_key/crypto.rs new file mode 100644 index 00000000000..1ee6c4cdf40 --- /dev/null +++ b/apps/desktop/desktop_native/core/src/secure_memory/secure_key/crypto.rs @@ -0,0 +1,96 @@ +use std::ptr::NonNull; + +use chacha20poly1305::{aead::Aead, Key, KeyInit}; +use rand::{rng, Rng}; + +pub(super) const KEY_SIZE: usize = 32; +pub(super) const NONCE_SIZE: usize = 24; + +/// The encryption performed here is xchacha-poly1305. Any tampering with the key or the ciphertexts will result +/// in a decryption failure and panic. The key's memory contents are protected from being swapped to disk +/// via mlock. +pub(super) struct MemoryEncryptionKey(NonNull<[u8]>); + +/// An encrypted memory blob that must be decrypted using the same key that it was encrypted with. +pub struct EncryptedMemory { + nonce: [u8; NONCE_SIZE], + ciphertext: Vec, +} + +impl MemoryEncryptionKey { + pub fn new() -> Self { + let mut key = [0u8; KEY_SIZE]; + rng().fill(&mut key); + MemoryEncryptionKey::from(&key) + } + + /// Encrypts the given plaintext using the key. + #[allow(unused)] + pub(super) fn encrypt(&self, plaintext: &[u8]) -> EncryptedMemory { + let cipher = chacha20poly1305::XChaCha20Poly1305::new(Key::from_slice(self.as_ref())); + let mut nonce = [0u8; NONCE_SIZE]; + rng().fill(&mut nonce); + let ciphertext = cipher + .encrypt(chacha20poly1305::XNonce::from_slice(&nonce), plaintext) + .expect("encryption should not fail"); + EncryptedMemory { nonce, ciphertext } + } + + /// Decrypts the given encrypted memory using the key. A decryption failure will panic. This is + /// okay because neither the keys nor ciphertexts should ever fail to decrypt, and doing so + /// indicates that the process memory was tampered with. + #[allow(unused)] + pub(super) fn decrypt(&self, encrypted: &EncryptedMemory) -> Result, DecryptionError> { + let cipher = chacha20poly1305::XChaCha20Poly1305::new(Key::from_slice(self.as_ref())); + cipher + .decrypt( + chacha20poly1305::XNonce::from_slice(&encrypted.nonce), + encrypted.ciphertext.as_ref(), + ) + .map_err(|_| DecryptionError::CouldNotDecrypt) + } +} + +impl Drop for MemoryEncryptionKey { + fn drop(&mut self) { + unsafe { + memsec::free(self.0); + } + } +} + +impl From<&[u8; KEY_SIZE]> for MemoryEncryptionKey { + fn from(value: &[u8; KEY_SIZE]) -> Self { + let mut ptr: NonNull<[u8]> = + unsafe { memsec::malloc_sized(KEY_SIZE).expect("malloc_sized should work") }; + unsafe { + std::ptr::copy_nonoverlapping(value.as_ptr(), ptr.as_mut().as_mut_ptr(), KEY_SIZE); + } + MemoryEncryptionKey(ptr) + } +} + +impl AsRef<[u8]> for MemoryEncryptionKey { + fn as_ref(&self) -> &[u8] { + unsafe { self.0.as_ref() } + } +} + +#[derive(Debug)] +pub(crate) enum DecryptionError { + CouldNotDecrypt, +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_memory_encryption_key() { + let key = MemoryEncryptionKey::new(); + let data = b"Hello, world!"; + let encrypted = key.encrypt(data); + let decrypted = key.decrypt(&encrypted).unwrap(); + assert_eq!(data.as_ref(), decrypted.as_slice()); + } +} diff --git a/apps/desktop/desktop_native/core/src/secure_memory/secure_key/dpapi.rs b/apps/desktop/desktop_native/core/src/secure_memory/secure_key/dpapi.rs new file mode 100644 index 00000000000..0975b542877 --- /dev/null +++ b/apps/desktop/desktop_native/core/src/secure_memory/secure_key/dpapi.rs @@ -0,0 +1,93 @@ +use super::crypto::{MemoryEncryptionKey, KEY_SIZE}; +use super::SecureKeyContainer; +use windows::Win32::Security::Cryptography::{ + CryptProtectMemory, CryptUnprotectMemory, CRYPTPROTECTMEMORY_BLOCK_SIZE, + CRYPTPROTECTMEMORY_SAME_PROCESS, +}; + +/// https://learn.microsoft.com/en-us/windows/win32/api/dpapi/nf-dpapi-cryptprotectdata +/// The DPAPI store encrypts data using the Windows Data Protection API (DPAPI). The key is bound +/// to the current process, and cannot be decrypted by other user-mode processes. +/// +/// Note: Admin processes can still decrypt this memory: +/// https://blog.slowerzs.net/posts/cryptdecryptmemory/ +pub(super) struct DpapiSecureKeyContainer { + dpapi_encrypted_key: [u8; KEY_SIZE + CRYPTPROTECTMEMORY_BLOCK_SIZE as usize], +} + +// SAFETY: The encrypted data is fully owned by this struct, and not exposed outside or cloned, +// and is disposed on drop of this struct. +unsafe impl Send for DpapiSecureKeyContainer {} +// SAFETY: The container is non-mutable and thus safe to share between threads. +unsafe impl Sync for DpapiSecureKeyContainer {} + +impl SecureKeyContainer for DpapiSecureKeyContainer { + fn as_key(&self) -> MemoryEncryptionKey { + let mut decrypted_key = self.dpapi_encrypted_key; + unsafe { + CryptUnprotectMemory( + decrypted_key.as_mut_ptr() as *mut core::ffi::c_void, + decrypted_key.len() as u32, + CRYPTPROTECTMEMORY_SAME_PROCESS, + ) + } + .expect("crypt_unprotect_memory should work"); + let mut key = [0u8; KEY_SIZE]; + key.copy_from_slice(&decrypted_key[..KEY_SIZE]); + MemoryEncryptionKey::from(&key) + } + + fn from_key(key: MemoryEncryptionKey) -> Self { + let mut padded_key = [0u8; KEY_SIZE + CRYPTPROTECTMEMORY_BLOCK_SIZE as usize]; + padded_key[..KEY_SIZE].copy_from_slice(key.as_ref()); + unsafe { + CryptProtectMemory( + padded_key.as_mut_ptr() as *mut core::ffi::c_void, + padded_key.len() as u32, + CRYPTPROTECTMEMORY_SAME_PROCESS, + ) + } + .expect("crypt_protect_memory should work"); + DpapiSecureKeyContainer { + dpapi_encrypted_key: padded_key, + } + } + + fn is_supported() -> bool { + // DPAPI is supported on all Windows versions that we support. + true + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_multiple_keys() { + let key1 = MemoryEncryptionKey::new(); + let key2 = MemoryEncryptionKey::new(); + let container1 = DpapiSecureKeyContainer::from_key(key1); + let container2 = DpapiSecureKeyContainer::from_key(key2); + + // Capture at time 1 + let data_1_1 = container1.as_key(); + let data_2_1 = container2.as_key(); + // Capture at time 2 + let data_1_2 = container1.as_key(); + let data_2_2 = container2.as_key(); + + // Same keys should be equal + assert_eq!(data_1_1.as_ref(), data_1_2.as_ref()); + assert_eq!(data_2_1.as_ref(), data_2_2.as_ref()); + + // Different keys should be different + assert_ne!(data_1_1.as_ref(), data_2_1.as_ref()); + assert_ne!(data_1_2.as_ref(), data_2_2.as_ref()); + } + + #[test] + fn test_is_supported() { + assert!(DpapiSecureKeyContainer::is_supported()); + } +} diff --git a/apps/desktop/desktop_native/core/src/secure_memory/secure_key/keyctl.rs b/apps/desktop/desktop_native/core/src/secure_memory/secure_key/keyctl.rs new file mode 100644 index 00000000000..a738d964671 --- /dev/null +++ b/apps/desktop/desktop_native/core/src/secure_memory/secure_key/keyctl.rs @@ -0,0 +1,100 @@ +use crate::secure_memory::secure_key::crypto::MemoryEncryptionKey; + +use super::crypto::KEY_SIZE; +use super::SecureKeyContainer; +use linux_keyutils::{KeyRing, KeyRingIdentifier}; + +/// The keys are bound to the process keyring. +const KEY_RING_IDENTIFIER: KeyRingIdentifier = KeyRingIdentifier::Process; +/// This is an atomic global counter used to help generate unique key IDs +static COUNTER: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(0); +/// Generates a unique ID for the key in the kernel keyring. +/// SAFETY: This function is safe to call from multiple threads because it uses an atomic counter. +fn make_id() -> String { + let counter = COUNTER.fetch_add(1, std::sync::atomic::Ordering::Relaxed); + // In case multiple processes are running, include the PID in the key ID. + let pid = std::process::id(); + format!("bitwarden_desktop_{}_{}", pid, counter) +} + +/// A secure key container that uses the Linux kernel keyctl API to store the key. +/// `https://man7.org/linux/man-pages/man1/keyctl.1.html`. The kernel enforces only +/// the correct process can read them, and they do not live in process memory space +/// and cannot be dumped. +pub(super) struct KeyctlSecureKeyContainer { + /// The kernel has an identifier for the key. This is randomly generated on construction. + id: String, +} + +// SAFETY: The key id is fully owned by this struct and not exposed or cloned, and cleaned up on drop. +// Further, since we use `KeyRingIdentifier::Process` and not `KeyRingIdentifier::Thread`, the key +// is accessible across threads within the same process bound. +unsafe impl Send for KeyctlSecureKeyContainer {} +// SAFETY: The container is non-mutable and thus safe to share between threads. +unsafe impl Sync for KeyctlSecureKeyContainer {} + +impl SecureKeyContainer for KeyctlSecureKeyContainer { + fn as_key(&self) -> MemoryEncryptionKey { + let ring = KeyRing::from_special_id(KEY_RING_IDENTIFIER, false) + .expect("should get process keyring"); + let key = ring.search(&self.id).expect("should find key"); + let mut buffer = [0u8; KEY_SIZE]; + key.read(&mut buffer).expect("should read key"); + MemoryEncryptionKey::from(&buffer) + } + + fn from_key(data: MemoryEncryptionKey) -> Self { + let ring = KeyRing::from_special_id(KEY_RING_IDENTIFIER, true) + .expect("should get process keyring"); + let id = make_id(); + ring.add_key(&id, &data).expect("should add key"); + KeyctlSecureKeyContainer { id } + } + + fn is_supported() -> bool { + KeyRing::from_special_id(KEY_RING_IDENTIFIER, true).is_ok() + } +} + +impl Drop for KeyctlSecureKeyContainer { + fn drop(&mut self) { + let ring = KeyRing::from_special_id(KEY_RING_IDENTIFIER, false) + .expect("should get process keyring"); + if let Ok(key) = ring.search(&self.id) { + let _ = key.invalidate(); + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_multiple_keys() { + let key1 = MemoryEncryptionKey::new(); + let key2 = MemoryEncryptionKey::new(); + let container1 = KeyctlSecureKeyContainer::from_key(key1); + let container2 = KeyctlSecureKeyContainer::from_key(key2); + + // Capture at time 1 + let data_1_1 = container1.as_key(); + let data_2_1 = container2.as_key(); + // Capture at time 2 + let data_1_2 = container1.as_key(); + let data_2_2 = container2.as_key(); + + // Same keys should be equal + assert_eq!(data_1_1.as_ref(), data_1_2.as_ref()); + assert_eq!(data_2_1.as_ref(), data_2_2.as_ref()); + + // Different keys should be different + assert_ne!(data_1_1.as_ref(), data_2_1.as_ref()); + assert_ne!(data_1_2.as_ref(), data_2_2.as_ref()); + } + + #[test] + fn test_is_supported() { + assert!(KeyctlSecureKeyContainer::is_supported()); + } +} diff --git a/apps/desktop/desktop_native/core/src/secure_memory/secure_key/memfd_secret.rs b/apps/desktop/desktop_native/core/src/secure_memory/secure_key/memfd_secret.rs new file mode 100644 index 00000000000..4e6a2c4d7ac --- /dev/null +++ b/apps/desktop/desktop_native/core/src/secure_memory/secure_key/memfd_secret.rs @@ -0,0 +1,109 @@ +use std::{ptr::NonNull, sync::LazyLock}; + +use super::crypto::MemoryEncryptionKey; +use super::crypto::KEY_SIZE; +use super::SecureKeyContainer; + +/// https://man.archlinux.org/man/memfd_secret.2.en +/// The memfd_secret store protects the data using the `memfd_secret` syscall. The +/// data is inaccessible to other user-mode processes, and even to root in most cases. +/// If arbitrary data can be executed in the kernel, the data can still be retrieved: +/// https://github.com/JonathonReinhart/nosecmem +pub(super) struct MemfdSecretSecureKeyContainer { + ptr: NonNull<[u8]>, +} +// SAFETY: The pointers in this struct are allocated by `memfd_secret`, and we have full ownership. +// They are never exposed outside or cloned, and are cleaned up by drop. +unsafe impl Send for MemfdSecretSecureKeyContainer {} +// SAFETY: The container is non-mutable and thus safe to share between threads. Further, memfd-secret +// is accessible across threads within the same process bound. +unsafe impl Sync for MemfdSecretSecureKeyContainer {} + +impl SecureKeyContainer for MemfdSecretSecureKeyContainer { + fn as_key(&self) -> MemoryEncryptionKey { + MemoryEncryptionKey::from( + &unsafe { self.ptr.as_ref() } + .try_into() + .expect("slice should be KEY_SIZE"), + ) + } + + fn from_key(key: MemoryEncryptionKey) -> Self { + let mut ptr: NonNull<[u8]> = unsafe { + memsec::memfd_secret_sized(KEY_SIZE).expect("memfd_secret_sized should work") + }; + unsafe { + std::ptr::copy_nonoverlapping( + key.as_ref().as_ptr(), + ptr.as_mut().as_mut_ptr(), + KEY_SIZE, + ); + } + MemfdSecretSecureKeyContainer { ptr } + } + + /// Note, `memfd_secret` is only available since Linux 6.5, so fallbacks are needed. + fn is_supported() -> bool { + // To test if memfd_secret is supported, we try to allocate a 1 byte and see if that + // succeeds. + static IS_SUPPORTED: LazyLock = LazyLock::new(|| { + let Some(ptr): Option> = (unsafe { memsec::memfd_secret_sized(1) }) + else { + return false; + }; + + // Check that the pointer is readable and writable + let result = unsafe { + let ptr = ptr.as_ptr() as *mut u8; + *ptr = 30; + *ptr += 107; + *ptr == 137 + }; + + unsafe { memsec::free_memfd_secret(ptr) }; + result + }); + *IS_SUPPORTED + } +} + +impl Drop for MemfdSecretSecureKeyContainer { + fn drop(&mut self) { + unsafe { + memsec::free_memfd_secret(self.ptr); + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_multiple_keys() { + let key1 = MemoryEncryptionKey::new(); + let key2 = MemoryEncryptionKey::new(); + let container1 = MemfdSecretSecureKeyContainer::from_key(key1); + let container2 = MemfdSecretSecureKeyContainer::from_key(key2); + + // Capture at time 1 + let data_1_1 = container1.as_key(); + let data_2_1 = container2.as_key(); + // Capture at time 2 + let data_1_2 = container1.as_key(); + let data_2_2 = container2.as_key(); + + // Same keys should be equal + assert_eq!(data_1_1.as_ref(), data_1_2.as_ref()); + assert_eq!(data_2_1.as_ref(), data_2_2.as_ref()); + + // Different keys should be different + assert_ne!(data_1_1.as_ref(), data_2_1.as_ref()); + assert_ne!(data_1_2.as_ref(), data_2_2.as_ref()); + } + + #[test] + fn test_is_supported() { + assert!(MemfdSecretSecureKeyContainer::is_supported()); + } +} diff --git a/apps/desktop/desktop_native/core/src/secure_memory/secure_key/mlock.rs b/apps/desktop/desktop_native/core/src/secure_memory/secure_key/mlock.rs new file mode 100644 index 00000000000..db21cd7fedc --- /dev/null +++ b/apps/desktop/desktop_native/core/src/secure_memory/secure_key/mlock.rs @@ -0,0 +1,83 @@ +use std::ptr::NonNull; + +use super::crypto::MemoryEncryptionKey; +use super::crypto::KEY_SIZE; +use super::SecureKeyContainer; + +/// A SecureKeyContainer that uses mlock to prevent the memory from being swapped to disk. +/// This does not provide as strong protections as other methods, but is always supported. +pub(super) struct MlockSecureKeyContainer { + ptr: NonNull<[u8]>, +} +// SAFETY: The pointers in this struct are allocated by `malloc_sized`, and we have full ownership. +// They are never exposed outside or cloned, and are cleaned up by drop. +unsafe impl Send for MlockSecureKeyContainer {} +// SAFETY: The container is non-mutable and thus safe to share between threads. +unsafe impl Sync for MlockSecureKeyContainer {} + +impl SecureKeyContainer for MlockSecureKeyContainer { + fn as_key(&self) -> MemoryEncryptionKey { + MemoryEncryptionKey::from( + &unsafe { self.ptr.as_ref() } + .try_into() + .expect("slice should be KEY_SIZE"), + ) + } + fn from_key(key: MemoryEncryptionKey) -> Self { + let mut ptr: NonNull<[u8]> = + unsafe { memsec::malloc_sized(KEY_SIZE).expect("malloc_sized should work") }; + unsafe { + std::ptr::copy_nonoverlapping( + key.as_ref().as_ptr(), + ptr.as_mut().as_mut_ptr(), + KEY_SIZE, + ); + } + MlockSecureKeyContainer { ptr } + } + + fn is_supported() -> bool { + true + } +} + +impl Drop for MlockSecureKeyContainer { + fn drop(&mut self) { + unsafe { + memsec::free(self.ptr); + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_multiple_keys() { + let key1 = MemoryEncryptionKey::new(); + let key2 = MemoryEncryptionKey::new(); + let container1 = MlockSecureKeyContainer::from_key(key1); + let container2 = MlockSecureKeyContainer::from_key(key2); + + // Capture at time 1 + let data_1_1 = container1.as_key(); + let data_2_1 = container2.as_key(); + // Capture at time 2 + let data_1_2 = container1.as_key(); + let data_2_2 = container2.as_key(); + + // Same keys should be equal + assert_eq!(data_1_1.as_ref(), data_1_2.as_ref()); + assert_eq!(data_2_1.as_ref(), data_2_2.as_ref()); + + // Different keys should be different + assert_ne!(data_1_1.as_ref(), data_2_1.as_ref()); + assert_ne!(data_1_2.as_ref(), data_2_2.as_ref()); + } + + #[test] + fn test_is_supported() { + assert!(MlockSecureKeyContainer::is_supported()); + } +} diff --git a/apps/desktop/desktop_native/core/src/secure_memory/secure_key/mod.rs b/apps/desktop/desktop_native/core/src/secure_memory/secure_key/mod.rs new file mode 100644 index 00000000000..6c3b53117a5 --- /dev/null +++ b/apps/desktop/desktop_native/core/src/secure_memory/secure_key/mod.rs @@ -0,0 +1,242 @@ +//! This module provides hardened storage for single cryptographic keys. These are meant for encrypting large amounts of memory. +//! Some platforms restrict how many keys can be protected by their APIs, which necessitates this layer of indirection. This significantly +//! reduces the complexity of each platform specific implementation, since all that's needed is implementing protecting a single fixed sized key +//! instead of protecting many arbitrarily sized secrets. This significantly lowers the effort to maintain each implementation. +//! +//! The implementations include DPAPI on Windows, `keyctl` on Linux, and `memfd_secret` on Linux, and a fallback implementation using mlock. + +use tracing::info; + +mod crypto; +#[cfg(target_os = "windows")] +mod dpapi; +#[cfg(target_os = "linux")] +mod keyctl; +#[cfg(target_os = "linux")] +mod memfd_secret; +mod mlock; + +pub use crypto::EncryptedMemory; + +use crate::secure_memory::secure_key::crypto::DecryptionError; + +/// An ephemeral key that is protected using a platform mechanism. It is generated on construction freshly, and can be used +/// to encrypt and decrypt segments of memory. Since the key is ephemeral, persistent data cannot be encrypted with this key. +/// On Linux and Windows, in most cases the protection mechanisms prevent memory dumps/debuggers from reading the key. +/// +/// Note: This can be circumvented if code can be injected into the process and is only effective in combination with the +/// memory isolation provided in `process_isolation`. +/// - https://github.com/zer1t0/keydump +#[allow(unused)] +pub(crate) struct SecureMemoryEncryptionKey(CrossPlatformSecureKeyContainer); + +impl SecureMemoryEncryptionKey { + pub fn new() -> Self { + SecureMemoryEncryptionKey(CrossPlatformSecureKeyContainer::from_key( + crypto::MemoryEncryptionKey::new(), + )) + } + + /// Encrypts the provided plaintext using the contained key, returning an EncryptedMemory blob. + #[allow(unused)] + pub fn encrypt(&self, plaintext: &[u8]) -> crypto::EncryptedMemory { + self.0.as_key().encrypt(plaintext) + } + + /// Decrypts the provided EncryptedMemory blob using the contained key, returning the plaintext. + /// If the decryption fails, that means the memory was tampered with, and the function panics. + #[allow(unused)] + pub fn decrypt(&self, encrypted: &crypto::EncryptedMemory) -> Result, DecryptionError> { + self.0.as_key().decrypt(encrypted) + } +} + +/// A platform specific implementation of a key container that protects a single encryption key +/// from memory attacks. +#[allow(unused)] +trait SecureKeyContainer: Sync + Send { + /// Returns the key as a byte slice. This slice does not have additional memory protections applied. + fn as_key(&self) -> crypto::MemoryEncryptionKey; + /// Creates a new SecureKeyContainer from the provided key. + fn from_key(key: crypto::MemoryEncryptionKey) -> Self; + /// Returns true if this platform supports this secure key container implementation. + fn is_supported() -> bool; +} + +#[allow(unused)] +enum CrossPlatformSecureKeyContainer { + #[cfg(target_os = "windows")] + Dpapi(dpapi::DpapiSecureKeyContainer), + #[cfg(target_os = "linux")] + Keyctl(keyctl::KeyctlSecureKeyContainer), + #[cfg(target_os = "linux")] + MemfdSecret(memfd_secret::MemfdSecretSecureKeyContainer), + Mlock(mlock::MlockSecureKeyContainer), +} + +impl SecureKeyContainer for CrossPlatformSecureKeyContainer { + fn as_key(&self) -> crypto::MemoryEncryptionKey { + match self { + #[cfg(target_os = "windows")] + CrossPlatformSecureKeyContainer::Dpapi(c) => c.as_key(), + #[cfg(target_os = "linux")] + CrossPlatformSecureKeyContainer::Keyctl(c) => c.as_key(), + #[cfg(target_os = "linux")] + CrossPlatformSecureKeyContainer::MemfdSecret(c) => c.as_key(), + CrossPlatformSecureKeyContainer::Mlock(c) => c.as_key(), + } + } + + fn from_key(key: crypto::MemoryEncryptionKey) -> Self { + if let Some(container) = get_env_forced_container() { + return container; + } + + #[cfg(target_os = "windows")] + { + if dpapi::DpapiSecureKeyContainer::is_supported() { + info!("Using DPAPI for secure key storage"); + return CrossPlatformSecureKeyContainer::Dpapi( + dpapi::DpapiSecureKeyContainer::from_key(key), + ); + } + } + #[cfg(target_os = "linux")] + { + // Memfd_secret is slightly better in some cases of the kernel being compromised. + // Note that keyctl may sometimes not be available in e.g. snap. Memfd_secret is + // not available on kernels older than 6.5 while keyctl is supported since 2.6. + // + // Note: This may prevent the system from hibernating but not sleeping. Hibernate + // would write the memory to disk, exposing the keys. If this is an issue, + // the environment variable `SECURE_KEY_CONTAINER_BACKEND` can be used + // to force the use of keyctl or mlock. + if memfd_secret::MemfdSecretSecureKeyContainer::is_supported() { + info!("Using memfd_secret for secure key storage"); + return CrossPlatformSecureKeyContainer::MemfdSecret( + memfd_secret::MemfdSecretSecureKeyContainer::from_key(key), + ); + } + if keyctl::KeyctlSecureKeyContainer::is_supported() { + info!("Using keyctl for secure key storage"); + return CrossPlatformSecureKeyContainer::Keyctl( + keyctl::KeyctlSecureKeyContainer::from_key(key), + ); + } + } + + // Falling back to mlock means that the key is accessible via memory dumping. + info!("Falling back to mlock for secure key storage"); + CrossPlatformSecureKeyContainer::Mlock(mlock::MlockSecureKeyContainer::from_key(key)) + } + + fn is_supported() -> bool { + // Mlock is always supported as a fallback. + true + } +} + +fn get_env_forced_container() -> Option { + let env_var = std::env::var("SECURE_KEY_CONTAINER_BACKEND"); + match env_var.as_deref() { + #[cfg(target_os = "windows")] + Ok("dpapi") => { + info!("Forcing DPAPI secure key container via environment variable"); + Some(CrossPlatformSecureKeyContainer::Dpapi( + dpapi::DpapiSecureKeyContainer::from_key(crypto::MemoryEncryptionKey::new()), + )) + } + #[cfg(target_os = "linux")] + Ok("memfd_secret") => { + info!("Forcing memfd_secret secure key container via environment variable"); + Some(CrossPlatformSecureKeyContainer::MemfdSecret( + memfd_secret::MemfdSecretSecureKeyContainer::from_key( + crypto::MemoryEncryptionKey::new(), + ), + )) + } + #[cfg(target_os = "linux")] + Ok("keyctl") => { + info!("Forcing keyctl secure key container via environment variable"); + Some(CrossPlatformSecureKeyContainer::Keyctl( + keyctl::KeyctlSecureKeyContainer::from_key(crypto::MemoryEncryptionKey::new()), + )) + } + Ok("mlock") => { + info!("Forcing mlock secure key container via environment variable"); + Some(CrossPlatformSecureKeyContainer::Mlock( + mlock::MlockSecureKeyContainer::from_key(crypto::MemoryEncryptionKey::new()), + )) + } + _ => { + info!( + "{} is not a valid secure key container backend, using automatic selection", + env_var.unwrap_or_default() + ); + None + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_multiple_keys() { + // Create 20 different keys + let original_keys: Vec = (0..20) + .map(|_| crypto::MemoryEncryptionKey::new()) + .collect(); + + // Store them in secure containers + let containers: Vec = original_keys + .iter() + .map(|key| { + let key_bytes: &[u8; crypto::KEY_SIZE] = key.as_ref().try_into().unwrap(); + CrossPlatformSecureKeyContainer::from_key(crypto::MemoryEncryptionKey::from( + key_bytes, + )) + }) + .collect(); + + // Read all keys back and validate they match the originals + for (i, (original_key, container)) in + original_keys.iter().zip(containers.iter()).enumerate() + { + let retrieved_key = container.as_key(); + assert_eq!( + original_key.as_ref(), + retrieved_key.as_ref(), + "Key {} should match after storage and retrieval", + i + ); + } + + // Verify all keys are different from each other + for i in 0..original_keys.len() { + for j in (i + 1)..original_keys.len() { + assert_ne!( + original_keys[i].as_ref(), + original_keys[j].as_ref(), + "Keys {} and {} should be different", + i, + j + ); + } + } + + // Read keys back a second time to ensure consistency + for (i, (original_key, container)) in + original_keys.iter().zip(containers.iter()).enumerate() + { + let retrieved_key_again = container.as_key(); + assert_eq!( + original_key.as_ref(), + retrieved_key_again.as_ref(), + "Key {} should still match on second retrieval", + i + ); + } + } +} diff --git a/apps/desktop/src/auth/components/set-pin.component.ts b/apps/desktop/src/auth/components/set-pin.component.ts index 93e1ea0d25c..5bb8e761b32 100644 --- a/apps/desktop/src/auth/components/set-pin.component.ts +++ b/apps/desktop/src/auth/components/set-pin.component.ts @@ -12,6 +12,8 @@ import { FormFieldModule, IconButtonModule, } from "@bitwarden/components"; +// FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush +// eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection @Component({ templateUrl: "set-pin.component.html", imports: [ diff --git a/apps/desktop/src/auth/delete-account.component.ts b/apps/desktop/src/auth/delete-account.component.ts index b6c6650375d..5cd73896e07 100644 --- a/apps/desktop/src/auth/delete-account.component.ts +++ b/apps/desktop/src/auth/delete-account.component.ts @@ -20,6 +20,8 @@ import { import { UserVerificationComponent } from "../app/components/user-verification.component"; +// FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush +// eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection @Component({ selector: "app-delete-account", templateUrl: "delete-account.component.html", diff --git a/apps/web/src/app/admin-console/organizations/policies/auto-confirm-edit-policy-dialog.component.html b/apps/web/src/app/admin-console/organizations/policies/auto-confirm-edit-policy-dialog.component.html new file mode 100644 index 00000000000..2388bb06bd8 --- /dev/null +++ b/apps/web/src/app/admin-console/organizations/policies/auto-confirm-edit-policy-dialog.component.html @@ -0,0 +1,91 @@ +
+ + + @let title = (multiStepSubmit | async)[currentStep()]?.titleContent(); + @if (title) { + + } + + + + @if (loading) { +
+ + {{ "loading" | i18n }} +
+ } +
+ @if (policy.showDescription) { +

{{ policy.description | i18n }}

+ } +
+ +
+ + @let footer = (multiStepSubmit | async)[currentStep()]?.footerContent(); + @if (footer) { + + } + +
+
+ + +
+ @let showBadge = firstTimeDialog(); + @if (showBadge) { + {{ "availableNow" | i18n }} + } + + {{ (firstTimeDialog ? "autoConfirm" : "editPolicy") | i18n }} + @if (!firstTimeDialog) { + + {{ policy.name | i18n }} + + } + +
+
+ + + {{ "howToTurnOnAutoConfirm" | i18n }} + + + + + + + + + + + diff --git a/apps/web/src/app/admin-console/organizations/policies/auto-confirm-edit-policy-dialog.component.ts b/apps/web/src/app/admin-console/organizations/policies/auto-confirm-edit-policy-dialog.component.ts new file mode 100644 index 00000000000..18a9306b7d1 --- /dev/null +++ b/apps/web/src/app/admin-console/organizations/policies/auto-confirm-edit-policy-dialog.component.ts @@ -0,0 +1,249 @@ +import { + AfterViewInit, + ChangeDetectorRef, + Component, + Inject, + signal, + Signal, + TemplateRef, + viewChild, +} from "@angular/core"; +import { FormBuilder } from "@angular/forms"; +import { Router } from "@angular/router"; +import { + combineLatest, + firstValueFrom, + map, + Observable, + of, + shareReplay, + startWith, + switchMap, + tap, +} from "rxjs"; + +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 { PolicyType } from "@bitwarden/common/admin-console/enums"; +import { PolicyRequest } from "@bitwarden/common/admin-console/models/request/policy.request"; +import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; +import { getUserId } from "@bitwarden/common/auth/services/account.service"; +import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; +import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; +import { + DIALOG_DATA, + DialogConfig, + DialogRef, + DialogService, + ToastService, +} from "@bitwarden/components"; +import { KeyService } from "@bitwarden/key-management"; + +import { SharedModule } from "../../../shared"; + +import { AutoConfirmPolicyEditComponent } from "./policy-edit-definitions/auto-confirm-policy.component"; +import { + PolicyEditDialogComponent, + PolicyEditDialogData, + PolicyEditDialogResult, +} from "./policy-edit-dialog.component"; + +export type MultiStepSubmit = { + sideEffect: () => Promise; + footerContent: Signal | undefined>; + titleContent: Signal | undefined>; +}; + +export type AutoConfirmPolicyDialogData = PolicyEditDialogData & { + firstTimeDialog?: boolean; +}; + +/** + * Custom policy dialog component for Auto-Confirm policy. + * Satisfies the PolicyDialogComponent interface structurally + * via its static open() function. + */ +@Component({ + templateUrl: "auto-confirm-edit-policy-dialog.component.html", + imports: [SharedModule], +}) +export class AutoConfirmPolicyDialogComponent + extends PolicyEditDialogComponent + implements AfterViewInit +{ + policyType = PolicyType; + + protected firstTimeDialog = signal(false); + protected currentStep = signal(0); + protected multiStepSubmit: Observable = of([]); + protected autoConfirmEnabled$: Observable = this.accountService.activeAccount$.pipe( + getUserId, + switchMap((userId) => this.policyService.policies$(userId)), + map((policies) => policies.find((p) => p.type === PolicyType.AutoConfirm)?.enabled ?? false), + ); + + private submitPolicy: Signal | undefined> = viewChild("step0"); + private openExtension: Signal | undefined> = viewChild("step1"); + + private submitPolicyTitle: Signal | undefined> = viewChild("step0Title"); + private openExtensionTitle: Signal | undefined> = viewChild("step1Title"); + + override policyComponent: AutoConfirmPolicyEditComponent | undefined; + + constructor( + @Inject(DIALOG_DATA) protected data: AutoConfirmPolicyDialogData, + accountService: AccountService, + policyApiService: PolicyApiServiceAbstraction, + i18nService: I18nService, + cdr: ChangeDetectorRef, + formBuilder: FormBuilder, + dialogRef: DialogRef, + toastService: ToastService, + configService: ConfigService, + keyService: KeyService, + private policyService: PolicyService, + private router: Router, + ) { + super( + data, + accountService, + policyApiService, + i18nService, + cdr, + formBuilder, + dialogRef, + toastService, + configService, + keyService, + ); + + this.firstTimeDialog.set(data.firstTimeDialog ?? false); + } + + /** + * Instantiates the child policy component and inserts it into the view. + */ + async ngAfterViewInit() { + await super.ngAfterViewInit(); + + if (this.policyComponent) { + this.saveDisabled$ = combineLatest([ + this.autoConfirmEnabled$, + this.policyComponent.enabled.valueChanges.pipe( + startWith(this.policyComponent.enabled.value), + ), + ]).pipe(map(([policyEnabled, value]) => !policyEnabled && !value)); + } + + this.multiStepSubmit = this.accountService.activeAccount$.pipe( + getUserId, + switchMap((userId) => this.policyService.policies$(userId)), + map((policies) => policies.find((p) => p.type === PolicyType.SingleOrg)?.enabled ?? false), + tap((singleOrgPolicyEnabled) => + this.policyComponent?.setSingleOrgEnabled(singleOrgPolicyEnabled), + ), + map((singleOrgPolicyEnabled) => [ + { + sideEffect: () => this.handleSubmit(singleOrgPolicyEnabled ?? false), + footerContent: this.submitPolicy, + titleContent: this.submitPolicyTitle, + }, + { + sideEffect: () => this.openBrowserExtension(), + footerContent: this.openExtension, + titleContent: this.openExtensionTitle, + }, + ]), + shareReplay({ bufferSize: 1, refCount: true }), + ); + } + + private async handleSubmit(singleOrgEnabled: boolean) { + if (!singleOrgEnabled) { + await this.submitSingleOrg(); + } + await this.submitAutoConfirm(); + } + + /** + * Triggers policy submission for auto confirm. + * @returns boolean: true if multi-submit workflow should continue, false otherwise. + */ + private async submitAutoConfirm() { + if (!this.policyComponent) { + throw new Error("PolicyComponent not initialized."); + } + + const autoConfirmRequest = await this.policyComponent.buildRequest(); + await this.policyApiService.putPolicy( + this.data.organizationId, + this.data.policy.type, + autoConfirmRequest, + ); + + this.toastService.showToast({ + variant: "success", + message: this.i18nService.t("editedPolicyId", this.i18nService.t(this.data.policy.name)), + }); + + if (!this.policyComponent.enabled.value) { + this.dialogRef.close("saved"); + } + } + + private async submitSingleOrg(): Promise { + const singleOrgRequest: PolicyRequest = { + type: PolicyType.SingleOrg, + enabled: true, + data: null, + }; + + await this.policyApiService.putPolicy( + this.data.organizationId, + PolicyType.SingleOrg, + singleOrgRequest, + ); + } + + private async openBrowserExtension() { + await this.router.navigate(["/browser-extension-prompt"], { + queryParams: { url: "AutoConfirm" }, + }); + } + + submit = async () => { + if (!this.policyComponent) { + throw new Error("PolicyComponent not initialized."); + } + + if ((await this.policyComponent.confirm()) == false) { + this.dialogRef.close(); + return; + } + + try { + const multiStepSubmit = await firstValueFrom(this.multiStepSubmit); + await multiStepSubmit[this.currentStep()].sideEffect(); + + if (this.currentStep() === multiStepSubmit.length - 1) { + this.dialogRef.close("saved"); + return; + } + + this.currentStep.update((value) => value + 1); + this.policyComponent.setStep(this.currentStep()); + } catch (error: any) { + this.toastService.showToast({ + variant: "error", + message: error.message, + }); + } + }; + + static open = ( + dialogService: DialogService, + config: DialogConfig, + ) => { + return dialogService.open(AutoConfirmPolicyDialogComponent, config); + }; +} diff --git a/apps/web/src/app/admin-console/organizations/policies/base-policy-edit.component.ts b/apps/web/src/app/admin-console/organizations/policies/base-policy-edit.component.ts index 9293c686b7f..9bf0ad24b1b 100644 --- a/apps/web/src/app/admin-console/organizations/policies/base-policy-edit.component.ts +++ b/apps/web/src/app/admin-console/organizations/policies/base-policy-edit.component.ts @@ -8,8 +8,20 @@ import { Organization } from "@bitwarden/common/admin-console/models/domain/orga import { PolicyRequest } from "@bitwarden/common/admin-console/models/request/policy.request"; import { PolicyResponse } from "@bitwarden/common/admin-console/models/response/policy.response"; import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; +import { DialogConfig, DialogRef, DialogService } from "@bitwarden/components"; -import type { PolicyEditDialogComponent } from "./policy-edit-dialog.component"; +import type { PolicyEditDialogData, PolicyEditDialogResult } from "./policy-edit-dialog.component"; + +/** + * Interface for policy dialog components. + * Any component that implements this interface can be used as a custom policy edit dialog. + */ +export interface PolicyDialogComponent { + open: ( + dialogService: DialogService, + config: DialogConfig, + ) => DialogRef; +} /** * A metadata class that defines how a policy is displayed in the Admin Console Policies page for editing. @@ -37,9 +49,8 @@ export abstract class BasePolicyEditDefinition { /** * The dialog component that will be opened when editing this policy. * This allows customizing the look and feel of each policy's dialog contents. - * If not specified, defaults to {@link PolicyEditDialogComponent}. */ - editDialogComponent?: typeof PolicyEditDialogComponent; + editDialogComponent?: PolicyDialogComponent; /** * If true, the {@link description} will be reused in the policy edit modal. Set this to false if you diff --git a/apps/web/src/app/admin-console/organizations/policies/policies.component.ts b/apps/web/src/app/admin-console/organizations/policies/policies.component.ts index 95c00f74f1c..7bab6f262a6 100644 --- a/apps/web/src/app/admin-console/organizations/policies/policies.component.ts +++ b/apps/web/src/app/admin-console/organizations/policies/policies.component.ts @@ -1,17 +1,18 @@ // FIXME: Update this file to be type safe and remove this and next line // @ts-strict-ignore import { Component, OnInit } from "@angular/core"; +import { takeUntilDestroyed } from "@angular/core/rxjs-interop"; import { ActivatedRoute } from "@angular/router"; import { combineLatest, firstValueFrom, - lastValueFrom, Observable, of, switchMap, first, map, withLatestFrom, + tap, } from "rxjs"; import { @@ -19,9 +20,11 @@ import { OrganizationService, } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.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 { PolicyType } from "@bitwarden/common/admin-console/enums"; import { PolicyResponse } from "@bitwarden/common/admin-console/models/response/policy.response"; import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; +import { getUserId } from "@bitwarden/common/auth/services/account.service"; import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; import { DialogService } from "@bitwarden/components"; import { safeProvider } from "@bitwarden/ui-common"; @@ -29,7 +32,7 @@ import { safeProvider } from "@bitwarden/ui-common"; import { HeaderModule } from "../../../layouts/header/header.module"; import { SharedModule } from "../../../shared"; -import { BasePolicyEditDefinition } from "./base-policy-edit.component"; +import { BasePolicyEditDefinition, PolicyDialogComponent } from "./base-policy-edit.component"; import { PolicyEditDialogComponent } from "./policy-edit-dialog.component"; import { PolicyListService } from "./policy-list.service"; import { POLICY_EDIT_REGISTER } from "./policy-register-token"; @@ -59,8 +62,18 @@ export class PoliciesComponent implements OnInit { private policyApiService: PolicyApiServiceAbstraction, private policyListService: PolicyListService, private dialogService: DialogService, + private policyService: PolicyService, protected configService: ConfigService, - ) {} + ) { + this.accountService.activeAccount$ + .pipe( + getUserId, + switchMap((userId) => this.policyService.policies$(userId)), + tap(async () => await this.load()), + takeUntilDestroyed(), + ) + .subscribe(); + } async ngOnInit() { // eslint-disable-next-line rxjs-angular/prefer-takeuntil, rxjs/no-async-subscribe @@ -127,17 +140,13 @@ export class PoliciesComponent implements OnInit { } async edit(policy: BasePolicyEditDefinition) { - const dialogComponent = policy.editDialogComponent ?? PolicyEditDialogComponent; - const dialogRef = dialogComponent.open(this.dialogService, { + const dialogComponent: PolicyDialogComponent = + policy.editDialogComponent ?? PolicyEditDialogComponent; + dialogComponent.open(this.dialogService, { data: { policy: policy, organizationId: this.organizationId, }, }); - - const result = await lastValueFrom(dialogRef.closed); - if (result == "saved") { - await this.load(); - } } } diff --git a/apps/web/src/app/admin-console/organizations/policies/policy-edit-definitions/auto-confirm-policy.component.html b/apps/web/src/app/admin-console/organizations/policies/policy-edit-definitions/auto-confirm-policy.component.html new file mode 100644 index 00000000000..8334b451d22 --- /dev/null +++ b/apps/web/src/app/admin-console/organizations/policies/policy-edit-definitions/auto-confirm-policy.component.html @@ -0,0 +1,59 @@ + + + +

+ {{ "autoConfirmPolicyEditDescription" | i18n }} +

+ +
    +
  • + + {{ "autoConfirmAcceptSecurityRiskTitle" | i18n }} + + {{ "autoConfirmAcceptSecurityRiskDescription" | i18n }} + + {{ "autoConfirmAcceptSecurityRiskLearnMore" | i18n }} + + +
  • + +
  • + @if (singleOrgEnabled$ | async) { + + {{ "autoConfirmSingleOrgExemption" | i18n }} + + } @else { + + {{ "autoConfirmSingleOrgRequired" | i18n }} + + } + {{ "autoConfirmSingleOrgRequiredDescription" | i18n }} +
  • + +
  • + + {{ "autoConfirmNoEmergencyAccess" | i18n }} + + {{ "autoConfirmNoEmergencyAccessDescription" | i18n }} +
  • +
+ + + {{ "autoConfirmCheckBoxLabel" | i18n }} +
+ + +
+ +
+
    +
  1. 1. {{ "autoConfirmStep1" | i18n }}
  2. + +
  3. + 2. {{ "autoConfirmStep2a" | i18n }} + + {{ "autoConfirmStep2b" | i18n }} + +
  4. +
+
diff --git a/apps/web/src/app/admin-console/organizations/policies/policy-edit-definitions/auto-confirm-policy.component.ts b/apps/web/src/app/admin-console/organizations/policies/policy-edit-definitions/auto-confirm-policy.component.ts new file mode 100644 index 00000000000..a5ea2ef8790 --- /dev/null +++ b/apps/web/src/app/admin-console/organizations/policies/policy-edit-definitions/auto-confirm-policy.component.ts @@ -0,0 +1,50 @@ +import { Component, OnInit, Signal, TemplateRef, viewChild } from "@angular/core"; +import { BehaviorSubject, map, Observable } from "rxjs"; + +import { AutoConfirmSvg } from "@bitwarden/assets/svg"; +import { PolicyType } from "@bitwarden/common/admin-console/enums"; +import { Organization } from "@bitwarden/common/admin-console/models/domain/organization"; +import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum"; +import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; + +import { SharedModule } from "../../../../shared"; +import { AutoConfirmPolicyDialogComponent } from "../auto-confirm-edit-policy-dialog.component"; +import { BasePolicyEditDefinition, BasePolicyEditComponent } from "../base-policy-edit.component"; + +export class AutoConfirmPolicy extends BasePolicyEditDefinition { + name = "autoConfirm"; + description = "autoConfirmDescription"; + type = PolicyType.AutoConfirm; + component = AutoConfirmPolicyEditComponent; + showDescription = false; + editDialogComponent = AutoConfirmPolicyDialogComponent; + + override display$(organization: Organization, configService: ConfigService): Observable { + return configService + .getFeatureFlag$(FeatureFlag.AutoConfirm) + .pipe(map((enabled) => enabled && organization.useAutomaticUserConfirmation)); + } +} + +@Component({ + templateUrl: "auto-confirm-policy.component.html", + imports: [SharedModule], +}) +export class AutoConfirmPolicyEditComponent extends BasePolicyEditComponent implements OnInit { + protected readonly autoConfirmSvg = AutoConfirmSvg; + private policyForm: Signal | undefined> = viewChild("step0"); + private extensionButton: Signal | undefined> = viewChild("step1"); + + protected step: number = 0; + protected steps = [this.policyForm, this.extensionButton]; + + protected singleOrgEnabled$: BehaviorSubject = new BehaviorSubject(false); + + setSingleOrgEnabled(enabled: boolean) { + this.singleOrgEnabled$.next(enabled); + } + + setStep(step: number) { + this.step = step; + } +} diff --git a/apps/web/src/app/admin-console/organizations/policies/policy-edit-definitions/index.ts b/apps/web/src/app/admin-console/organizations/policies/policy-edit-definitions/index.ts index bb2c40b7a76..7373e1ff888 100644 --- a/apps/web/src/app/admin-console/organizations/policies/policy-edit-definitions/index.ts +++ b/apps/web/src/app/admin-console/organizations/policies/policy-edit-definitions/index.ts @@ -14,3 +14,4 @@ export { vNextOrganizationDataOwnershipPolicy, vNextOrganizationDataOwnershipPolicyComponent, } from "./vnext-organization-data-ownership.component"; +export { AutoConfirmPolicy } from "./auto-confirm-policy.component"; diff --git a/apps/web/src/app/admin-console/organizations/policies/policy-edit-dialog.component.ts b/apps/web/src/app/admin-console/organizations/policies/policy-edit-dialog.component.ts index f0672d0f861..d98b5d4809b 100644 --- a/apps/web/src/app/admin-console/organizations/policies/policy-edit-dialog.component.ts +++ b/apps/web/src/app/admin-console/organizations/policies/policy-edit-dialog.component.ts @@ -30,7 +30,7 @@ import { KeyService } from "@bitwarden/key-management"; import { SharedModule } from "../../../shared"; import { BasePolicyEditDefinition, BasePolicyEditComponent } from "./base-policy-edit.component"; -import { vNextOrganizationDataOwnershipPolicyComponent } from "./policy-edit-definitions"; +import { vNextOrganizationDataOwnershipPolicyComponent } from "./policy-edit-definitions/vnext-organization-data-ownership.component"; export type PolicyEditDialogData = { /** @@ -64,13 +64,13 @@ export class PolicyEditDialogComponent implements AfterViewInit { }); constructor( @Inject(DIALOG_DATA) protected data: PolicyEditDialogData, - private accountService: AccountService, - private policyApiService: PolicyApiServiceAbstraction, - private i18nService: I18nService, + protected accountService: AccountService, + protected policyApiService: PolicyApiServiceAbstraction, + protected i18nService: I18nService, private cdr: ChangeDetectorRef, private formBuilder: FormBuilder, - private dialogRef: DialogRef, - private toastService: ToastService, + protected dialogRef: DialogRef, + protected toastService: ToastService, private configService: ConfigService, private keyService: KeyService, ) {} diff --git a/apps/web/src/app/admin-console/organizations/policies/policy-edit-register.ts b/apps/web/src/app/admin-console/organizations/policies/policy-edit-register.ts index 5e63ba1358a..ca44818764c 100644 --- a/apps/web/src/app/admin-console/organizations/policies/policy-edit-register.ts +++ b/apps/web/src/app/admin-console/organizations/policies/policy-edit-register.ts @@ -1,5 +1,6 @@ import { BasePolicyEditDefinition } from "./base-policy-edit.component"; import { + AutoConfirmPolicy, DesktopAutotypeDefaultSettingPolicy, DisableSendPolicy, MasterPasswordPolicy, @@ -33,4 +34,5 @@ export const ossPolicyEditRegister: BasePolicyEditDefinition[] = [ new SendOptionsPolicy(), new RestrictedItemTypesPolicy(), new DesktopAutotypeDefaultSettingPolicy(), + new AutoConfirmPolicy(), ]; diff --git a/apps/web/src/app/auth/emergency-access/accept/accept-emergency.component.ts b/apps/web/src/app/auth/emergency-access/accept/accept-emergency.component.ts index e1b7329504c..d1491e6d782 100644 --- a/apps/web/src/app/auth/emergency-access/accept/accept-emergency.component.ts +++ b/apps/web/src/app/auth/emergency-access/accept/accept-emergency.component.ts @@ -12,6 +12,8 @@ import { SharedModule } from "../../../shared"; import { EmergencyAccessModule } from "../emergency-access.module"; import { EmergencyAccessService } from "../services/emergency-access.service"; +// FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush +// eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection @Component({ imports: [SharedModule, EmergencyAccessModule], templateUrl: "accept-emergency.component.html", diff --git a/apps/web/src/app/auth/guards/deep-link/deep-link.guard.spec.ts b/apps/web/src/app/auth/guards/deep-link/deep-link.guard.spec.ts index dba4dbd8357..31033b29154 100644 --- a/apps/web/src/app/auth/guards/deep-link/deep-link.guard.spec.ts +++ b/apps/web/src/app/auth/guards/deep-link/deep-link.guard.spec.ts @@ -11,18 +11,24 @@ import { RouterService } from "../../../core/router.service"; import { deepLinkGuard } from "./deep-link.guard"; +// FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush +// eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection @Component({ template: "", standalone: false, }) export class GuardedRouteTestComponent {} +// FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush +// eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection @Component({ template: "", standalone: false, }) export class LockTestComponent {} +// FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush +// eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection @Component({ template: "", standalone: false, diff --git a/apps/web/src/app/auth/organization-invite/accept-organization.component.ts b/apps/web/src/app/auth/organization-invite/accept-organization.component.ts index 09d4fc3e9ef..f98a62f91ea 100644 --- a/apps/web/src/app/auth/organization-invite/accept-organization.component.ts +++ b/apps/web/src/app/auth/organization-invite/accept-organization.component.ts @@ -16,6 +16,8 @@ import { BaseAcceptComponent } from "../../common/base.accept.component"; import { AcceptOrganizationInviteService } from "./accept-organization.service"; +// FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush +// eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection @Component({ templateUrl: "accept-organization.component.html", standalone: false, diff --git a/apps/web/src/app/auth/recover-delete.component.ts b/apps/web/src/app/auth/recover-delete.component.ts index 7381d526879..00b14f9a402 100644 --- a/apps/web/src/app/auth/recover-delete.component.ts +++ b/apps/web/src/app/auth/recover-delete.component.ts @@ -10,6 +10,8 @@ import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.servic import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; import { ToastService } from "@bitwarden/components"; +// FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush +// eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection @Component({ selector: "app-recover-delete", templateUrl: "recover-delete.component.html", diff --git a/apps/web/src/app/auth/recover-two-factor.component.ts b/apps/web/src/app/auth/recover-two-factor.component.ts index f606e803df3..dc85668c8ec 100644 --- a/apps/web/src/app/auth/recover-two-factor.component.ts +++ b/apps/web/src/app/auth/recover-two-factor.component.ts @@ -16,6 +16,8 @@ import { LogService } from "@bitwarden/common/platform/abstractions/log.service" import { ValidationService } from "@bitwarden/common/platform/abstractions/validation.service"; import { ToastService } from "@bitwarden/components"; +// FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush +// eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection @Component({ selector: "app-recover-two-factor", templateUrl: "recover-two-factor.component.html", diff --git a/apps/web/src/app/auth/settings/account/account.component.ts b/apps/web/src/app/auth/settings/account/account.component.ts index 921db19bc49..8bae8cd2c1f 100644 --- a/apps/web/src/app/auth/settings/account/account.component.ts +++ b/apps/web/src/app/auth/settings/account/account.component.ts @@ -19,6 +19,8 @@ import { DeleteAccountDialogComponent } from "./delete-account-dialog.component" import { ProfileComponent } from "./profile.component"; import { SetAccountVerifyDevicesDialogComponent } from "./set-account-verify-devices-dialog.component"; +// FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush +// eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection @Component({ templateUrl: "account.component.html", imports: [ diff --git a/apps/web/src/app/auth/settings/account/change-avatar-dialog.component.ts b/apps/web/src/app/auth/settings/account/change-avatar-dialog.component.ts index 6bb785fb8f5..6e6fac1404e 100644 --- a/apps/web/src/app/auth/settings/account/change-avatar-dialog.component.ts +++ b/apps/web/src/app/auth/settings/account/change-avatar-dialog.component.ts @@ -32,6 +32,8 @@ type ChangeAvatarDialogData = { profile: ProfileResponse; }; +// FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush +// eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection @Component({ templateUrl: "change-avatar-dialog.component.html", encapsulation: ViewEncapsulation.None, @@ -40,6 +42,8 @@ type ChangeAvatarDialogData = { export class ChangeAvatarDialogComponent implements OnInit, OnDestroy { profile: ProfileResponse; + // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals + // eslint-disable-next-line @angular-eslint/prefer-signals @ViewChild("colorPicker") colorPickerElement: ElementRef; loading = false; diff --git a/apps/web/src/app/auth/settings/account/change-email.component.ts b/apps/web/src/app/auth/settings/account/change-email.component.ts index b6ca39c6413..ee29e0c8a9c 100644 --- a/apps/web/src/app/auth/settings/account/change-email.component.ts +++ b/apps/web/src/app/auth/settings/account/change-email.component.ts @@ -17,6 +17,8 @@ import { KdfConfigService, KeyService } from "@bitwarden/key-management"; import { SharedModule } from "../../../shared"; +// FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush +// eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection @Component({ selector: "app-change-email", templateUrl: "change-email.component.html", diff --git a/apps/web/src/app/auth/settings/account/danger-zone.component.ts b/apps/web/src/app/auth/settings/account/danger-zone.component.ts index 05fd22d087d..e60ff6ec03d 100644 --- a/apps/web/src/app/auth/settings/account/danger-zone.component.ts +++ b/apps/web/src/app/auth/settings/account/danger-zone.component.ts @@ -9,6 +9,8 @@ import { I18nPipe } from "@bitwarden/ui-common"; /** * Component for the Danger Zone section of the Account/Organization Settings page. */ +// FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush +// eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection @Component({ selector: "app-danger-zone", templateUrl: "danger-zone.component.html", diff --git a/apps/web/src/app/auth/settings/account/deauthorize-sessions.component.ts b/apps/web/src/app/auth/settings/account/deauthorize-sessions.component.ts index f75320e8335..b792963ae9b 100644 --- a/apps/web/src/app/auth/settings/account/deauthorize-sessions.component.ts +++ b/apps/web/src/app/auth/settings/account/deauthorize-sessions.component.ts @@ -12,6 +12,8 @@ import { DialogService, ToastService } from "@bitwarden/components"; import { SharedModule } from "../../../shared"; +// FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush +// eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection @Component({ templateUrl: "deauthorize-sessions.component.html", imports: [SharedModule, UserVerificationFormInputComponent], diff --git a/apps/web/src/app/auth/settings/account/delete-account-dialog.component.ts b/apps/web/src/app/auth/settings/account/delete-account-dialog.component.ts index 7e8f169994f..76eb067fdd2 100644 --- a/apps/web/src/app/auth/settings/account/delete-account-dialog.component.ts +++ b/apps/web/src/app/auth/settings/account/delete-account-dialog.component.ts @@ -12,6 +12,8 @@ import { DialogRef, DialogService, ToastService } from "@bitwarden/components"; import { SharedModule } from "../../../shared"; +// FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush +// eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection @Component({ templateUrl: "delete-account-dialog.component.html", imports: [SharedModule, UserVerificationFormInputComponent], diff --git a/apps/web/src/app/auth/settings/account/profile.component.ts b/apps/web/src/app/auth/settings/account/profile.component.ts index 1f4fa578491..fd96f343b3a 100644 --- a/apps/web/src/app/auth/settings/account/profile.component.ts +++ b/apps/web/src/app/auth/settings/account/profile.component.ts @@ -23,6 +23,8 @@ import { AccountFingerprintComponent } from "../../../shared/components/account- import { ChangeAvatarDialogComponent } from "./change-avatar-dialog.component"; +// FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush +// eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection @Component({ selector: "app-profile", templateUrl: "profile.component.html", diff --git a/apps/web/src/app/auth/settings/account/selectable-avatar.component.ts b/apps/web/src/app/auth/settings/account/selectable-avatar.component.ts index 630c0e949ad..b74cf8beb59 100644 --- a/apps/web/src/app/auth/settings/account/selectable-avatar.component.ts +++ b/apps/web/src/app/auth/settings/account/selectable-avatar.component.ts @@ -5,6 +5,8 @@ import { Component, EventEmitter, Input, Output } from "@angular/core"; import { AvatarModule } from "@bitwarden/components"; +// FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush +// eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection @Component({ selector: "selectable-avatar", template: `(); onFire() { diff --git a/apps/web/src/app/auth/settings/account/set-account-verify-devices-dialog.component.ts b/apps/web/src/app/auth/settings/account/set-account-verify-devices-dialog.component.ts index c66f31f6c3b..01be46c45b3 100644 --- a/apps/web/src/app/auth/settings/account/set-account-verify-devices-dialog.component.ts +++ b/apps/web/src/app/auth/settings/account/set-account-verify-devices-dialog.component.ts @@ -27,6 +27,8 @@ import { ToastService, } from "@bitwarden/components"; +// FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush +// eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection @Component({ templateUrl: "./set-account-verify-devices-dialog.component.html", imports: [ diff --git a/apps/web/src/app/auth/settings/emergency-access/confirm/emergency-access-confirm.component.ts b/apps/web/src/app/auth/settings/emergency-access/confirm/emergency-access-confirm.component.ts index 641dde66cc4..a5fdd5212fa 100644 --- a/apps/web/src/app/auth/settings/emergency-access/confirm/emergency-access-confirm.component.ts +++ b/apps/web/src/app/auth/settings/emergency-access/confirm/emergency-access-confirm.component.ts @@ -25,6 +25,8 @@ type EmergencyAccessConfirmDialogData = { /** user public key */ publicKey: Uint8Array; }; +// FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush +// eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection @Component({ templateUrl: "emergency-access-confirm.component.html", imports: [SharedModule], diff --git a/apps/web/src/app/auth/settings/emergency-access/emergency-access-add-edit.component.ts b/apps/web/src/app/auth/settings/emergency-access/emergency-access-add-edit.component.ts index 04b549e7f05..2e8d02a0c4f 100644 --- a/apps/web/src/app/auth/settings/emergency-access/emergency-access-add-edit.component.ts +++ b/apps/web/src/app/auth/settings/emergency-access/emergency-access-add-edit.component.ts @@ -35,6 +35,8 @@ export enum EmergencyAccessAddEditDialogResult { Canceled = "canceled", Deleted = "deleted", } +// FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush +// eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection @Component({ templateUrl: "emergency-access-add-edit.component.html", imports: [SharedModule, PremiumBadgeComponent], diff --git a/apps/web/src/app/auth/settings/emergency-access/emergency-access.component.ts b/apps/web/src/app/auth/settings/emergency-access/emergency-access.component.ts index f6594f4b11a..c2b8127ec34 100644 --- a/apps/web/src/app/auth/settings/emergency-access/emergency-access.component.ts +++ b/apps/web/src/app/auth/settings/emergency-access/emergency-access.component.ts @@ -42,6 +42,8 @@ import { EmergencyAccessTakeoverDialogResultType, } from "./takeover/emergency-access-takeover-dialog.component"; +// FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush +// eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection @Component({ templateUrl: "emergency-access.component.html", imports: [SharedModule, HeaderModule, PremiumBadgeComponent], diff --git a/apps/web/src/app/auth/settings/emergency-access/takeover/emergency-access-takeover-dialog.component.ts b/apps/web/src/app/auth/settings/emergency-access/takeover/emergency-access-takeover-dialog.component.ts index e5c21fb82b9..743f41537e9 100644 --- a/apps/web/src/app/auth/settings/emergency-access/takeover/emergency-access-takeover-dialog.component.ts +++ b/apps/web/src/app/auth/settings/emergency-access/takeover/emergency-access-takeover-dialog.component.ts @@ -48,6 +48,8 @@ export type EmergencyAccessTakeoverDialogResultType = * * @link https://bitwarden.com/help/emergency-access/ */ +// FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush +// eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection @Component({ selector: "auth-emergency-access-takeover-dialog", templateUrl: "./emergency-access-takeover-dialog.component.html", @@ -61,6 +63,8 @@ export type EmergencyAccessTakeoverDialogResultType = ], }) export class EmergencyAccessTakeoverDialogComponent implements OnInit { + // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals + // eslint-disable-next-line @angular-eslint/prefer-signals @ViewChild(InputPasswordComponent) inputPasswordComponent: InputPasswordComponent | undefined = undefined; diff --git a/apps/web/src/app/auth/settings/emergency-access/view/emergency-access-view.component.ts b/apps/web/src/app/auth/settings/emergency-access/view/emergency-access-view.component.ts index 250261fb0e7..1d96a19ca74 100644 --- a/apps/web/src/app/auth/settings/emergency-access/view/emergency-access-view.component.ts +++ b/apps/web/src/app/auth/settings/emergency-access/view/emergency-access-view.component.ts @@ -14,6 +14,8 @@ import { EmergencyAccessService } from "../../../emergency-access"; import { EmergencyViewDialogComponent } from "./emergency-view-dialog.component"; +// FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush +// eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection @Component({ templateUrl: "emergency-access-view.component.html", providers: [{ provide: CipherFormConfigService, useClass: DefaultCipherFormConfigService }], diff --git a/apps/web/src/app/auth/settings/emergency-access/view/emergency-view-dialog.component.ts b/apps/web/src/app/auth/settings/emergency-access/view/emergency-view-dialog.component.ts index 656ec894f27..62cfd95ecfa 100644 --- a/apps/web/src/app/auth/settings/emergency-access/view/emergency-view-dialog.component.ts +++ b/apps/web/src/app/auth/settings/emergency-access/view/emergency-view-dialog.component.ts @@ -35,6 +35,8 @@ class PremiumUpgradePromptNoop implements PremiumUpgradePromptService { } } +// FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush +// eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection @Component({ selector: "app-emergency-view-dialog", templateUrl: "emergency-view-dialog.component.html", diff --git a/apps/web/src/app/auth/settings/security/api-key.component.ts b/apps/web/src/app/auth/settings/security/api-key.component.ts index 82d1010f020..af49ca556ab 100644 --- a/apps/web/src/app/auth/settings/security/api-key.component.ts +++ b/apps/web/src/app/auth/settings/security/api-key.component.ts @@ -23,6 +23,8 @@ export type ApiKeyDialogData = { apiKeyWarning: string; apiKeyDescription: string; }; +// FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush +// eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection @Component({ templateUrl: "api-key.component.html", imports: [SharedModule, UserVerificationFormInputComponent], diff --git a/apps/web/src/app/auth/settings/security/password-settings/password-settings.component.ts b/apps/web/src/app/auth/settings/security/password-settings/password-settings.component.ts index 0698ffe1f8d..0e37c856935 100644 --- a/apps/web/src/app/auth/settings/security/password-settings/password-settings.component.ts +++ b/apps/web/src/app/auth/settings/security/password-settings/password-settings.component.ts @@ -10,6 +10,8 @@ import { I18nPipe } from "@bitwarden/ui-common"; import { WebauthnLoginSettingsModule } from "../../webauthn-login-settings"; +// FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush +// eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection @Component({ selector: "app-password-settings", templateUrl: "password-settings.component.html", diff --git a/apps/web/src/app/auth/settings/security/security-keys.component.ts b/apps/web/src/app/auth/settings/security/security-keys.component.ts index 9d16d4380eb..27a555ff343 100644 --- a/apps/web/src/app/auth/settings/security/security-keys.component.ts +++ b/apps/web/src/app/auth/settings/security/security-keys.component.ts @@ -13,6 +13,8 @@ import { SharedModule } from "../../../shared"; import { ApiKeyComponent } from "./api-key.component"; +// FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush +// eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection @Component({ templateUrl: "security-keys.component.html", imports: [SharedModule, ChangeKdfModule], diff --git a/apps/web/src/app/auth/settings/security/security.component.ts b/apps/web/src/app/auth/settings/security/security.component.ts index 2a237bf6d01..ff13515eec0 100644 --- a/apps/web/src/app/auth/settings/security/security.component.ts +++ b/apps/web/src/app/auth/settings/security/security.component.ts @@ -5,6 +5,8 @@ import { UserVerificationService } from "@bitwarden/common/auth/abstractions/use import { HeaderModule } from "../../../layouts/header/header.module"; import { SharedModule } from "../../../shared"; +// FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush +// eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection @Component({ templateUrl: "security.component.html", imports: [SharedModule, HeaderModule], diff --git a/apps/web/src/app/auth/settings/two-factor/two-factor-recovery.component.ts b/apps/web/src/app/auth/settings/two-factor/two-factor-recovery.component.ts index 37d94bfae0e..543c4236d89 100644 --- a/apps/web/src/app/auth/settings/two-factor/two-factor-recovery.component.ts +++ b/apps/web/src/app/auth/settings/two-factor/two-factor-recovery.component.ts @@ -15,6 +15,8 @@ import { } from "@bitwarden/components"; import { I18nPipe } from "@bitwarden/ui-common"; +// FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush +// eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection @Component({ selector: "app-two-factor-recovery", templateUrl: "two-factor-recovery.component.html", diff --git a/apps/web/src/app/auth/settings/two-factor/two-factor-setup-authenticator.component.ts b/apps/web/src/app/auth/settings/two-factor/two-factor-setup-authenticator.component.ts index d57d6eca894..20c3c742db6 100644 --- a/apps/web/src/app/auth/settings/two-factor/two-factor-setup-authenticator.component.ts +++ b/apps/web/src/app/auth/settings/two-factor/two-factor-setup-authenticator.component.ts @@ -53,6 +53,8 @@ declare global { } } +// FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush +// eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection @Component({ selector: "app-two-factor-setup-authenticator", templateUrl: "two-factor-setup-authenticator.component.html", @@ -76,6 +78,8 @@ export class TwoFactorSetupAuthenticatorComponent extends TwoFactorSetupMethodBaseComponent implements OnInit, OnDestroy { + // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals + // eslint-disable-next-line @angular-eslint/prefer-output-emitter-ref @Output() onChangeStatus = new EventEmitter(); type = TwoFactorProviderType.Authenticator; key: string; diff --git a/apps/web/src/app/auth/settings/two-factor/two-factor-setup-duo.component.ts b/apps/web/src/app/auth/settings/two-factor/two-factor-setup-duo.component.ts index bf820e32917..1a476f2206d 100644 --- a/apps/web/src/app/auth/settings/two-factor/two-factor-setup-duo.component.ts +++ b/apps/web/src/app/auth/settings/two-factor/two-factor-setup-duo.component.ts @@ -30,6 +30,8 @@ import { I18nPipe } from "@bitwarden/ui-common"; import { TwoFactorSetupMethodBaseComponent } from "./two-factor-setup-method-base.component"; +// FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush +// eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection @Component({ selector: "app-two-factor-setup-duo", templateUrl: "two-factor-setup-duo.component.html", @@ -51,6 +53,8 @@ export class TwoFactorSetupDuoComponent extends TwoFactorSetupMethodBaseComponent implements OnInit { + // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals + // eslint-disable-next-line @angular-eslint/prefer-output-emitter-ref @Output() onChangeStatus: EventEmitter = new EventEmitter(); type = TwoFactorProviderType.Duo; diff --git a/apps/web/src/app/auth/settings/two-factor/two-factor-setup-email.component.ts b/apps/web/src/app/auth/settings/two-factor/two-factor-setup-email.component.ts index 138d541d551..4219fb0b687 100644 --- a/apps/web/src/app/auth/settings/two-factor/two-factor-setup-email.component.ts +++ b/apps/web/src/app/auth/settings/two-factor/two-factor-setup-email.component.ts @@ -33,6 +33,8 @@ import { I18nPipe } from "@bitwarden/ui-common"; import { TwoFactorSetupMethodBaseComponent } from "./two-factor-setup-method-base.component"; +// FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush +// eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection @Component({ selector: "app-two-factor-setup-email", templateUrl: "two-factor-setup-email.component.html", @@ -54,6 +56,8 @@ export class TwoFactorSetupEmailComponent extends TwoFactorSetupMethodBaseComponent implements OnInit { + // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals + // eslint-disable-next-line @angular-eslint/prefer-output-emitter-ref @Output() onChangeStatus: EventEmitter = new EventEmitter(); type = TwoFactorProviderType.Email; sentEmail: string = ""; diff --git a/apps/web/src/app/auth/settings/two-factor/two-factor-setup-method-base.component.ts b/apps/web/src/app/auth/settings/two-factor/two-factor-setup-method-base.component.ts index aa3b9e1def3..c614e45e577 100644 --- a/apps/web/src/app/auth/settings/two-factor/two-factor-setup-method-base.component.ts +++ b/apps/web/src/app/auth/settings/two-factor/two-factor-setup-method-base.component.ts @@ -17,6 +17,8 @@ import { DialogService, ToastService } from "@bitwarden/components"; */ @Directive({}) export abstract class TwoFactorSetupMethodBaseComponent { + // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals + // eslint-disable-next-line @angular-eslint/prefer-output-emitter-ref @Output() onUpdated = new EventEmitter(); type: TwoFactorProviderType | undefined; diff --git a/apps/web/src/app/auth/settings/two-factor/two-factor-setup-webauthn.component.ts b/apps/web/src/app/auth/settings/two-factor/two-factor-setup-webauthn.component.ts index ff0e971461e..acf83ab278e 100644 --- a/apps/web/src/app/auth/settings/two-factor/two-factor-setup-webauthn.component.ts +++ b/apps/web/src/app/auth/settings/two-factor/two-factor-setup-webauthn.component.ts @@ -43,6 +43,8 @@ interface Key { removePromise: Promise | null; } +// FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush +// eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection @Component({ selector: "app-two-factor-setup-webauthn", templateUrl: "two-factor-setup-webauthn.component.html", diff --git a/apps/web/src/app/auth/settings/two-factor/two-factor-setup-yubikey.component.ts b/apps/web/src/app/auth/settings/two-factor/two-factor-setup-yubikey.component.ts index 4e4691a5f60..09fb1ad308f 100644 --- a/apps/web/src/app/auth/settings/two-factor/two-factor-setup-yubikey.component.ts +++ b/apps/web/src/app/auth/settings/two-factor/two-factor-setup-yubikey.component.ts @@ -44,6 +44,8 @@ interface Key { existingKey: string; } +// FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush +// eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection @Component({ selector: "app-two-factor-setup-yubikey", templateUrl: "two-factor-setup-yubikey.component.html", diff --git a/apps/web/src/app/auth/settings/two-factor/two-factor-setup.component.ts b/apps/web/src/app/auth/settings/two-factor/two-factor-setup.component.ts index c3a55ad661e..ef4d647a7d0 100644 --- a/apps/web/src/app/auth/settings/two-factor/two-factor-setup.component.ts +++ b/apps/web/src/app/auth/settings/two-factor/two-factor-setup.component.ts @@ -45,6 +45,8 @@ import { TwoFactorSetupWebAuthnComponent } from "./two-factor-setup-webauthn.com import { TwoFactorSetupYubiKeyComponent } from "./two-factor-setup-yubikey.component"; import { TwoFactorVerifyComponent } from "./two-factor-verify.component"; +// FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush +// eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection @Component({ selector: "app-two-factor-setup", templateUrl: "two-factor-setup.component.html", diff --git a/apps/web/src/app/auth/settings/two-factor/two-factor-verify.component.ts b/apps/web/src/app/auth/settings/two-factor/two-factor-verify.component.ts index a2c734ed2d5..9baa93d38c0 100644 --- a/apps/web/src/app/auth/settings/two-factor/two-factor-verify.component.ts +++ b/apps/web/src/app/auth/settings/two-factor/two-factor-verify.component.ts @@ -28,6 +28,8 @@ type TwoFactorVerifyDialogData = { organizationId: string; }; +// FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush +// eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection @Component({ selector: "app-two-factor-verify", templateUrl: "two-factor-verify.component.html", @@ -43,6 +45,8 @@ type TwoFactorVerifyDialogData = { export class TwoFactorVerifyComponent { type: TwoFactorProviderType; organizationId: string; + // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals + // eslint-disable-next-line @angular-eslint/prefer-output-emitter-ref @Output() onAuthed = new EventEmitter>(); formPromise: Promise | undefined; diff --git a/apps/web/src/app/auth/settings/verify-email.component.ts b/apps/web/src/app/auth/settings/verify-email.component.ts index 7088dae8d0f..a63d0b18b36 100644 --- a/apps/web/src/app/auth/settings/verify-email.component.ts +++ b/apps/web/src/app/auth/settings/verify-email.component.ts @@ -16,6 +16,8 @@ import { ToastService, } from "@bitwarden/components"; +// FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush +// eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection @Component({ selector: "app-verify-email", templateUrl: "verify-email.component.html", @@ -24,7 +26,11 @@ import { export class VerifyEmailComponent { actionPromise: Promise; + // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals + // eslint-disable-next-line @angular-eslint/prefer-output-emitter-ref @Output() onVerified = new EventEmitter(); + // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals + // eslint-disable-next-line @angular-eslint/prefer-output-emitter-ref @Output() onDismiss = new EventEmitter(); constructor( diff --git a/apps/web/src/app/auth/settings/webauthn-login-settings/create-credential-dialog/create-credential-dialog.component.ts b/apps/web/src/app/auth/settings/webauthn-login-settings/create-credential-dialog/create-credential-dialog.component.ts index 04b148e8a0a..89b7410baba 100644 --- a/apps/web/src/app/auth/settings/webauthn-login-settings/create-credential-dialog/create-credential-dialog.component.ts +++ b/apps/web/src/app/auth/settings/webauthn-login-settings/create-credential-dialog/create-credential-dialog.component.ts @@ -32,6 +32,8 @@ type Step = | "credentialCreationFailed" | "credentialNaming"; +// FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush +// eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection @Component({ templateUrl: "create-credential-dialog.component.html", standalone: false, diff --git a/apps/web/src/app/auth/settings/webauthn-login-settings/delete-credential-dialog/delete-credential-dialog.component.ts b/apps/web/src/app/auth/settings/webauthn-login-settings/delete-credential-dialog/delete-credential-dialog.component.ts index ea766a302ca..af4b7c497fb 100644 --- a/apps/web/src/app/auth/settings/webauthn-login-settings/delete-credential-dialog/delete-credential-dialog.component.ts +++ b/apps/web/src/app/auth/settings/webauthn-login-settings/delete-credential-dialog/delete-credential-dialog.component.ts @@ -24,6 +24,8 @@ export interface DeleteCredentialDialogParams { credentialId: string; } +// FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush +// eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection @Component({ templateUrl: "delete-credential-dialog.component.html", standalone: false, diff --git a/apps/web/src/app/auth/settings/webauthn-login-settings/enable-encryption-dialog/enable-encryption-dialog.component.ts b/apps/web/src/app/auth/settings/webauthn-login-settings/enable-encryption-dialog/enable-encryption-dialog.component.ts index dd1ac45a9b6..24a711cb5b4 100644 --- a/apps/web/src/app/auth/settings/webauthn-login-settings/enable-encryption-dialog/enable-encryption-dialog.component.ts +++ b/apps/web/src/app/auth/settings/webauthn-login-settings/enable-encryption-dialog/enable-encryption-dialog.component.ts @@ -21,6 +21,8 @@ export interface EnableEncryptionDialogParams { credentialId: string; } +// FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush +// eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection @Component({ templateUrl: "enable-encryption-dialog.component.html", standalone: false, diff --git a/apps/web/src/app/auth/settings/webauthn-login-settings/webauthn-login-settings.component.ts b/apps/web/src/app/auth/settings/webauthn-login-settings/webauthn-login-settings.component.ts index 94e926ac138..e8a278d8dd7 100644 --- a/apps/web/src/app/auth/settings/webauthn-login-settings/webauthn-login-settings.component.ts +++ b/apps/web/src/app/auth/settings/webauthn-login-settings/webauthn-login-settings.component.ts @@ -17,6 +17,8 @@ import { openCreateCredentialDialog } from "./create-credential-dialog/create-cr import { openDeleteCredentialDialogComponent } from "./delete-credential-dialog/delete-credential-dialog.component"; import { openEnableCredentialDialogComponent } from "./enable-encryption-dialog/enable-encryption-dialog.component"; +// FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush +// eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection @Component({ selector: "app-webauthn-login-settings", templateUrl: "webauthn-login-settings.component.html", diff --git a/apps/web/src/app/auth/shared/components/user-verification/user-verification-prompt.component.ts b/apps/web/src/app/auth/shared/components/user-verification/user-verification-prompt.component.ts index 77df374f3ed..beafa48bb8e 100644 --- a/apps/web/src/app/auth/shared/components/user-verification/user-verification-prompt.component.ts +++ b/apps/web/src/app/auth/shared/components/user-verification/user-verification-prompt.component.ts @@ -21,6 +21,8 @@ import { /** * @deprecated Jan 24, 2024: Use new libs/auth UserVerificationDialogComponent instead. */ +// FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush +// eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection @Component({ templateUrl: "user-verification-prompt.component.html", standalone: false, diff --git a/apps/web/src/app/auth/shared/components/user-verification/user-verification.component.ts b/apps/web/src/app/auth/shared/components/user-verification/user-verification.component.ts index 42f4b26fb36..7ea5014254b 100644 --- a/apps/web/src/app/auth/shared/components/user-verification/user-verification.component.ts +++ b/apps/web/src/app/auth/shared/components/user-verification/user-verification.component.ts @@ -8,6 +8,8 @@ import { UserVerificationComponent as BaseComponent } from "@bitwarden/angular/a * @deprecated Jan 24, 2024: Use new libs/auth UserVerificationDialogComponent or UserVerificationFormInputComponent instead. * Each client specific component should eventually be converted over to use one of these new components. */ +// FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush +// eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection @Component({ selector: "app-user-verification", templateUrl: "user-verification.component.html", diff --git a/apps/web/src/app/auth/verify-email-token.component.ts b/apps/web/src/app/auth/verify-email-token.component.ts index 2c4fa7f447c..30bfcf95bbf 100644 --- a/apps/web/src/app/auth/verify-email-token.component.ts +++ b/apps/web/src/app/auth/verify-email-token.component.ts @@ -13,6 +13,8 @@ import { LogService } from "@bitwarden/common/platform/abstractions/log.service" import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; import { ToastService } from "@bitwarden/components"; +// FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush +// eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection @Component({ selector: "app-verify-email-token", templateUrl: "verify-email-token.component.html", diff --git a/apps/web/src/app/auth/verify-recover-delete.component.ts b/apps/web/src/app/auth/verify-recover-delete.component.ts index a475fdfd3e5..06d6096c3de 100644 --- a/apps/web/src/app/auth/verify-recover-delete.component.ts +++ b/apps/web/src/app/auth/verify-recover-delete.component.ts @@ -11,6 +11,8 @@ import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.servic import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; import { ToastService } from "@bitwarden/components"; +// FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush +// eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection @Component({ selector: "app-verify-recover-delete", templateUrl: "verify-recover-delete.component.html", diff --git a/apps/web/src/app/billing/individual/premium/premium-vnext.component.ts b/apps/web/src/app/billing/individual/premium/premium-vnext.component.ts index 9de9c22d3c3..61994fdb61d 100644 --- a/apps/web/src/app/billing/individual/premium/premium-vnext.component.ts +++ b/apps/web/src/app/billing/individual/premium/premium-vnext.component.ts @@ -1,21 +1,29 @@ import { CommonModule } from "@angular/common"; import { Component, DestroyRef, inject } from "@angular/core"; import { takeUntilDestroyed } from "@angular/core/rxjs-interop"; -import { combineLatest, firstValueFrom, map, Observable, of, shareReplay, switchMap } from "rxjs"; +import { ActivatedRoute, Router } from "@angular/router"; +import { + combineLatest, + firstValueFrom, + from, + map, + Observable, + of, + shareReplay, + switchMap, +} from "rxjs"; import { ApiService } from "@bitwarden/common/abstractions/api.service"; import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions"; -import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; import { SyncService } from "@bitwarden/common/platform/sync"; import { - DialogService, - ToastService, - SectionComponent, BadgeModule, - TypographyModule, + DialogService, LinkModule, + SectionComponent, + TypographyModule, } from "@bitwarden/components"; import { PricingCardComponent } from "@bitwarden/pricing"; import { I18nPipe } from "@bitwarden/ui-common"; @@ -69,14 +77,14 @@ export class PremiumVNextComponent { constructor( private accountService: AccountService, - private i18nService: I18nService, private apiService: ApiService, private dialogService: DialogService, private platformUtilsService: PlatformUtilsService, private syncService: SyncService, - private toastService: ToastService, private billingAccountProfileStateService: BillingAccountProfileStateService, private subscriptionPricingService: SubscriptionPricingService, + private router: Router, + private activatedRoute: ActivatedRoute, ) { this.isSelfHost = this.platformUtilsService.isSelfHost(); @@ -107,6 +115,23 @@ export class PremiumVNextComponent { this.hasPremiumPersonally$, ]).pipe(map(([hasOrgPremium, hasPersonalPremium]) => !hasOrgPremium && !hasPersonalPremium)); + // redirect to user subscription page if they already have premium personally + // redirect to individual vault if they already have premium from an org + combineLatest([this.hasPremiumFromAnyOrganization$, this.hasPremiumPersonally$]) + .pipe( + takeUntilDestroyed(this.destroyRef), + switchMap(([hasPremiumFromOrg, hasPremiumPersonally]) => { + if (hasPremiumPersonally) { + return from(this.navigateToSubscriptionPage()); + } + if (hasPremiumFromOrg) { + return from(this.navigateToIndividualVault()); + } + return of(true); + }), + ) + .subscribe(); + this.personalPricingTiers$ = this.subscriptionPricingService.getPersonalSubscriptionPricingTiers$(); @@ -141,6 +166,11 @@ export class PremiumVNextComponent { ); } + private navigateToSubscriptionPage = (): Promise => + this.router.navigate(["../user-subscription"], { relativeTo: this.activatedRoute }); + + private navigateToIndividualVault = (): Promise => this.router.navigate(["/vault"]); + finalizeUpgrade = async () => { await this.apiService.refreshIdentityToken(); await this.syncService.fullSync(true); diff --git a/apps/web/src/app/key-management/change-kdf/change-kdf-confirmation.component.html b/apps/web/src/app/key-management/change-kdf/change-kdf-confirmation.component.html index 9f21b28f190..88c6c8b9aca 100644 --- a/apps/web/src/app/key-management/change-kdf/change-kdf-confirmation.component.html +++ b/apps/web/src/app/key-management/change-kdf/change-kdf-confirmation.component.html @@ -1,12 +1,14 @@
- {{ "changeKdf" | i18n }} + {{ "updateYourEncryptionSettings" | i18n }} - {{ "kdfSettingsChangeLogoutWarning" | i18n }} - + @if (!(noLogoutOnKdfChangeFeatureFlag$ | async)) { + {{ "kdfSettingsChangeLogoutWarning" | i18n }} + } + {{ "masterPass" | i18n }} {{ "confirmIdentity" | i18n }} - + + - - {{ "kdfMemory" | i18n }} - -
-
- + @if (isPBKDF2(kdfConfig)) { + {{ "kdfIterations" | i18n }} - - - {{ "kdfIterationRecommends" | i18n }} - - - - {{ "kdfIterations" | i18n }} - - - - - - {{ "kdfParallelism" | i18n }} - - - - -
+ } @else if (isArgon2(kdfConfig)) { + + {{ "kdfMemory" | i18n }} + + + }
+ @if (isArgon2(kdfConfig)) { +
+ + + {{ "kdfIterations" | i18n }} + + + +
+
+ + + {{ "kdfParallelism" | i18n }} + + + +
+ } + + +
    +
  • {{ "encryptionKeySettingsAlgorithmPopoverPBKDF2" | i18n }}
  • +
  • {{ "encryptionKeySettingsAlgorithmPopoverArgon2Id" | i18n }}
  • +
+ +
diff --git a/apps/web/src/app/key-management/change-kdf/change-kdf.component.spec.ts b/apps/web/src/app/key-management/change-kdf/change-kdf.component.spec.ts new file mode 100644 index 00000000000..c5144223ba0 --- /dev/null +++ b/apps/web/src/app/key-management/change-kdf/change-kdf.component.spec.ts @@ -0,0 +1,365 @@ +import { ComponentFixture, TestBed } from "@angular/core/testing"; +import { FormBuilder, FormControl } from "@angular/forms"; +import { mock, MockProxy } from "jest-mock-extended"; +import { of } from "rxjs"; + +import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; +import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; +import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; +import { FakeAccountService, mockAccountServiceWith } from "@bitwarden/common/spec"; +import { UserId } from "@bitwarden/common/types/guid"; +import { DialogService, PopoverModule, CalloutModule } from "@bitwarden/components"; +import { + KdfConfigService, + Argon2KdfConfig, + PBKDF2KdfConfig, + KdfType, +} from "@bitwarden/key-management"; + +import { SharedModule } from "../../shared"; + +import { ChangeKdfComponent } from "./change-kdf.component"; + +describe("ChangeKdfComponent", () => { + let component: ChangeKdfComponent; + let fixture: ComponentFixture; + + // Mock Services + let mockDialogService: MockProxy; + let mockKdfConfigService: MockProxy; + let mockConfigService: MockProxy; + let mockI18nService: MockProxy; + let accountService: FakeAccountService; + let formBuilder: FormBuilder; + + const mockUserId = "user-id" as UserId; + + // Helper functions for validation testing + function expectPBKDF2Validation( + iterationsControl: FormControl, + memoryControl: FormControl, + parallelismControl: FormControl, + ) { + // Assert current validators state + expect(iterationsControl.hasError("required")).toBe(false); + expect(iterationsControl.hasError("min")).toBe(false); + expect(iterationsControl.hasError("max")).toBe(false); + expect(memoryControl.validator).toBeNull(); + expect(parallelismControl.validator).toBeNull(); + + // Test validation boundaries + iterationsControl.setValue(PBKDF2KdfConfig.ITERATIONS.min - 1); + expect(iterationsControl.hasError("min")).toBe(true); + + iterationsControl.setValue(PBKDF2KdfConfig.ITERATIONS.max + 1); + expect(iterationsControl.hasError("max")).toBe(true); + } + + function expectArgon2Validation( + iterationsControl: FormControl, + memoryControl: FormControl, + parallelismControl: FormControl, + ) { + // Assert current validators state + expect(iterationsControl.hasError("required")).toBe(false); + expect(memoryControl.hasError("required")).toBe(false); + expect(parallelismControl.hasError("required")).toBe(false); + + // Test validation boundaries - min values + iterationsControl.setValue(Argon2KdfConfig.ITERATIONS.min - 1); + expect(iterationsControl.hasError("min")).toBe(true); + + memoryControl.setValue(Argon2KdfConfig.MEMORY.min - 1); + expect(memoryControl.hasError("min")).toBe(true); + + parallelismControl.setValue(Argon2KdfConfig.PARALLELISM.min - 1); + expect(parallelismControl.hasError("min")).toBe(true); + + // Test validation boundaries - max values + iterationsControl.setValue(Argon2KdfConfig.ITERATIONS.max + 1); + expect(iterationsControl.hasError("max")).toBe(true); + + memoryControl.setValue(Argon2KdfConfig.MEMORY.max + 1); + expect(memoryControl.hasError("max")).toBe(true); + + parallelismControl.setValue(Argon2KdfConfig.PARALLELISM.max + 1); + expect(parallelismControl.hasError("max")).toBe(true); + } + + beforeEach(() => { + mockDialogService = mock(); + mockKdfConfigService = mock(); + mockConfigService = mock(); + mockI18nService = mock(); + accountService = mockAccountServiceWith(mockUserId); + formBuilder = new FormBuilder(); + + mockConfigService.getFeatureFlag$.mockReturnValue(of(false)); + + mockI18nService.t.mockImplementation((key) => `${key}-used-i18n`); + + TestBed.configureTestingModule({ + declarations: [ChangeKdfComponent], + imports: [SharedModule, PopoverModule, CalloutModule], + providers: [ + { provide: DialogService, useValue: mockDialogService }, + { provide: KdfConfigService, useValue: mockKdfConfigService }, + { provide: AccountService, useValue: accountService }, + { provide: FormBuilder, useValue: formBuilder }, + { provide: ConfigService, useValue: mockConfigService }, + { provide: I18nService, useValue: mockI18nService }, + ], + }); + }); + + describe("Component Initialization", () => { + describe("given PBKDF2 configuration", () => { + it("should initialize form with PBKDF2 values and validators when component loads", async () => { + // Arrange + const mockPBKDF2Config = new PBKDF2KdfConfig(600_000); + mockKdfConfigService.getKdfConfig.mockResolvedValue(mockPBKDF2Config); + + // Act + fixture = TestBed.createComponent(ChangeKdfComponent); + component = fixture.componentInstance; + await component.ngOnInit(); + + // Extract form controls + const formGroup = component["formGroup"]; + + // Assert form values + expect(formGroup.controls.kdf.value).toBe(KdfType.PBKDF2_SHA256); + const kdfConfigFormGroup = formGroup.controls.kdfConfig; + expect(kdfConfigFormGroup.controls.iterations.value).toBe(600_000); + expect(kdfConfigFormGroup.controls.memory.value).toBeNull(); + expect(kdfConfigFormGroup.controls.parallelism.value).toBeNull(); + expect(component.kdfConfig).toEqual(mockPBKDF2Config); + + // Assert validators + expectPBKDF2Validation( + kdfConfigFormGroup.controls.iterations, + kdfConfigFormGroup.controls.memory, + kdfConfigFormGroup.controls.parallelism, + ); + }); + }); + + describe("given Argon2id configuration", () => { + it("should initialize form with Argon2id values and validators when component loads", async () => { + // Arrange + const mockArgon2Config = new Argon2KdfConfig(3, 64, 4); + mockKdfConfigService.getKdfConfig.mockResolvedValue(mockArgon2Config); + + // Act + fixture = TestBed.createComponent(ChangeKdfComponent); + component = fixture.componentInstance; + await component.ngOnInit(); + + // Extract form controls + const formGroup = component["formGroup"]; + + // Assert form values + expect(formGroup.controls.kdf.value).toBe(KdfType.Argon2id); + const kdfConfigFormGroup = formGroup.controls.kdfConfig; + expect(kdfConfigFormGroup.controls.iterations.value).toBe(3); + expect(kdfConfigFormGroup.controls.memory.value).toBe(64); + expect(kdfConfigFormGroup.controls.parallelism.value).toBe(4); + expect(component.kdfConfig).toEqual(mockArgon2Config); + + // Assert validators + expectArgon2Validation( + kdfConfigFormGroup.controls.iterations, + kdfConfigFormGroup.controls.memory, + kdfConfigFormGroup.controls.parallelism, + ); + }); + }); + + it.each([ + [true, false], + [false, true], + ])( + "should show log out banner = %s when feature flag observable is %s", + async (showLogOutBanner, forceUpgradeKdfFeatureFlag) => { + // Arrange + const mockPBKDF2Config = new PBKDF2KdfConfig(600_000); + mockKdfConfigService.getKdfConfig.mockResolvedValue(mockPBKDF2Config); + mockConfigService.getFeatureFlag$.mockReturnValue(of(forceUpgradeKdfFeatureFlag)); + + // Act + fixture = TestBed.createComponent(ChangeKdfComponent); + component = fixture.componentInstance; + await component.ngOnInit(); + fixture.detectChanges(); + + // Assert + const calloutElement = fixture.debugElement.query((el) => + el.nativeElement.textContent?.includes("kdfSettingsChangeLogoutWarning"), + ); + + if (showLogOutBanner) { + expect(calloutElement).not.toBeNull(); + expect(calloutElement.nativeElement.textContent).toContain( + "kdfSettingsChangeLogoutWarning-used-i18n", + ); + } else { + expect(calloutElement).toBeNull(); + } + }, + ); + }); + + describe("KDF Type Switching", () => { + describe("switching from PBKDF2 to Argon2id", () => { + beforeEach(async () => { + // Setup component with initial PBKDF2 configuration + const mockPBKDF2Config = new PBKDF2KdfConfig(600_001); + mockKdfConfigService.getKdfConfig.mockResolvedValue(mockPBKDF2Config); + + fixture = TestBed.createComponent(ChangeKdfComponent); + component = fixture.componentInstance; + await component.ngOnInit(); + }); + + it("should update form structure and default values when KDF type changes to Argon2id", () => { + // Arrange + const formGroup = component["formGroup"]; + + // Act - change KDF type to Argon2id + formGroup.controls.kdf.setValue(KdfType.Argon2id); + + // Assert form values update to Argon2id defaults + expect(formGroup.controls.kdf.value).toBe(KdfType.Argon2id); + const kdfConfigFormGroup = formGroup.controls.kdfConfig; + expect(kdfConfigFormGroup.controls.iterations.value).toBe(3); // Argon2id default + expect(kdfConfigFormGroup.controls.memory.value).toBe(64); // Argon2id default + expect(kdfConfigFormGroup.controls.parallelism.value).toBe(4); // Argon2id default + }); + + it("should update validators when KDF type changes to Argon2id", () => { + // Arrange + const formGroup = component["formGroup"]; + + // Act - change KDF type to Argon2id + formGroup.controls.kdf.setValue(KdfType.Argon2id); + + // Assert validators update to Argon2id validation rules + const kdfConfigFormGroup = formGroup.controls.kdfConfig; + expectArgon2Validation( + kdfConfigFormGroup.controls.iterations, + kdfConfigFormGroup.controls.memory, + kdfConfigFormGroup.controls.parallelism, + ); + }); + }); + + describe("switching from Argon2id to PBKDF2", () => { + beforeEach(async () => { + // Setup component with initial Argon2id configuration + const mockArgon2IdConfig = new Argon2KdfConfig(4, 65, 5); + mockKdfConfigService.getKdfConfig.mockResolvedValue(mockArgon2IdConfig); + + fixture = TestBed.createComponent(ChangeKdfComponent); + component = fixture.componentInstance; + await component.ngOnInit(); + }); + + it("should update form structure and default values when KDF type changes to PBKDF2", () => { + // Arrange + const formGroup = component["formGroup"]; + + // Act - change KDF type back to PBKDF2 + formGroup.controls.kdf.setValue(KdfType.PBKDF2_SHA256); + + // Assert form values update to PBKDF2 defaults + expect(formGroup.controls.kdf.value).toBe(KdfType.PBKDF2_SHA256); + const kdfConfigFormGroup = formGroup.controls.kdfConfig; + expect(kdfConfigFormGroup.controls.iterations.value).toBe(600_000); // PBKDF2 default + expect(kdfConfigFormGroup.controls.memory.value).toBeNull(); // PBKDF2 doesn't use memory + expect(kdfConfigFormGroup.controls.parallelism.value).toBeNull(); // PBKDF2 doesn't use parallelism + }); + + it("should update validators when KDF type changes to PBKDF2", () => { + // Arrange + const formGroup = component["formGroup"]; + + // Act - change KDF type back to PBKDF2 + formGroup.controls.kdf.setValue(KdfType.PBKDF2_SHA256); + + // Assert validators update to PBKDF2 validation rules + const kdfConfigFormGroup = formGroup.controls.kdfConfig; + expectPBKDF2Validation( + kdfConfigFormGroup.controls.iterations, + kdfConfigFormGroup.controls.memory, + kdfConfigFormGroup.controls.parallelism, + ); + }); + }); + }); + + describe("openConfirmationModal", () => { + describe("when form is valid", () => { + it("should open confirmation modal with PBKDF2 config when form is submitted", async () => { + // Arrange + const mockPBKDF2Config = new PBKDF2KdfConfig(600_001); + mockKdfConfigService.getKdfConfig.mockResolvedValue(mockPBKDF2Config); + + fixture = TestBed.createComponent(ChangeKdfComponent); + component = fixture.componentInstance; + await component.ngOnInit(); + + // Act + await component.openConfirmationModal(); + + // Assert + expect(mockDialogService.open).toHaveBeenCalledWith( + expect.any(Function), + expect.objectContaining({ + data: expect.objectContaining({ + kdfConfig: mockPBKDF2Config, + }), + }), + ); + }); + + it("should open confirmation modal with Argon2id config when form is submitted", async () => { + // Arrange + const mockArgon2Config = new Argon2KdfConfig(4, 65, 5); + mockKdfConfigService.getKdfConfig.mockResolvedValue(mockArgon2Config); + + fixture = TestBed.createComponent(ChangeKdfComponent); + component = fixture.componentInstance; + await component.ngOnInit(); + + // Act + await component.openConfirmationModal(); + + // Assert + expect(mockDialogService.open).toHaveBeenCalledWith( + expect.any(Function), + expect.objectContaining({ + data: expect.objectContaining({ + kdfConfig: mockArgon2Config, + }), + }), + ); + }); + + it("should not open modal when form is invalid", async () => { + // Arrange + const mockPBKDF2Config = new PBKDF2KdfConfig(PBKDF2KdfConfig.ITERATIONS.min - 1); + mockKdfConfigService.getKdfConfig.mockResolvedValue(mockPBKDF2Config); + + fixture = TestBed.createComponent(ChangeKdfComponent); + component = fixture.componentInstance; + await component.ngOnInit(); + + // Act + await component.openConfirmationModal(); + + // Assert + expect(mockDialogService.open).not.toHaveBeenCalled(); + }); + }); + }); +}); diff --git a/apps/web/src/app/key-management/change-kdf/change-kdf.component.ts b/apps/web/src/app/key-management/change-kdf/change-kdf.component.ts index 0463c6d4afc..f128aefdd9b 100644 --- a/apps/web/src/app/key-management/change-kdf/change-kdf.component.ts +++ b/apps/web/src/app/key-management/change-kdf/change-kdf.component.ts @@ -1,11 +1,11 @@ -// FIXME: Update this file to be type safe and remove this and next line -// @ts-strict-ignore import { Component, OnDestroy, OnInit } from "@angular/core"; -import { FormBuilder, FormControl, ValidatorFn, Validators } from "@angular/forms"; -import { Subject, firstValueFrom, takeUntil } from "rxjs"; +import { FormBuilder, FormControl, Validators } from "@angular/forms"; +import { Subject, firstValueFrom, takeUntil, Observable } from "rxjs"; import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; import { getUserId } from "@bitwarden/common/auth/services/account.service"; +import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum"; +import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; import { DialogService } from "@bitwarden/components"; import { KdfConfigService, @@ -31,11 +31,11 @@ export class ChangeKdfComponent implements OnInit, OnDestroy { private destroy$ = new Subject(); protected formGroup = this.formBuilder.group({ - kdf: new FormControl(KdfType.PBKDF2_SHA256, [Validators.required]), + kdf: new FormControl(KdfType.PBKDF2_SHA256, [Validators.required]), kdfConfig: this.formBuilder.group({ - iterations: [this.kdfConfig.iterations], - memory: [null as number], - parallelism: [null as number], + iterations: new FormControl(null), + memory: new FormControl(null), + parallelism: new FormControl(null), }), }); @@ -45,95 +45,102 @@ export class ChangeKdfComponent implements OnInit, OnDestroy { protected ARGON2_MEMORY = Argon2KdfConfig.MEMORY; protected ARGON2_PARALLELISM = Argon2KdfConfig.PARALLELISM; + noLogoutOnKdfChangeFeatureFlag$: Observable; + constructor( private dialogService: DialogService, private kdfConfigService: KdfConfigService, private accountService: AccountService, private formBuilder: FormBuilder, + configService: ConfigService, ) { this.kdfOptions = [ { name: "PBKDF2 SHA-256", value: KdfType.PBKDF2_SHA256 }, { name: "Argon2id", value: KdfType.Argon2id }, ]; + this.noLogoutOnKdfChangeFeatureFlag$ = configService.getFeatureFlag$( + FeatureFlag.NoLogoutOnKdfChange, + ); } async ngOnInit() { const userId = await firstValueFrom(getUserId(this.accountService.activeAccount$)); this.kdfConfig = await this.kdfConfigService.getKdfConfig(userId); - this.formGroup.get("kdf").setValue(this.kdfConfig.kdfType); + this.formGroup.controls.kdf.setValue(this.kdfConfig.kdfType); this.setFormControlValues(this.kdfConfig); + this.setFormValidators(this.kdfConfig.kdfType); - this.formGroup - .get("kdf") - .valueChanges.pipe(takeUntil(this.destroy$)) + this.formGroup.controls.kdf.valueChanges + .pipe(takeUntil(this.destroy$)) .subscribe((newValue) => { - this.updateKdfConfig(newValue); + this.updateKdfConfig(newValue!); }); } private updateKdfConfig(newValue: KdfType) { let config: KdfConfig; - const validators: { [key: string]: ValidatorFn[] } = { - iterations: [], - memory: [], - parallelism: [], - }; switch (newValue) { case KdfType.PBKDF2_SHA256: config = new PBKDF2KdfConfig(); - validators.iterations = [ - Validators.required, - Validators.min(PBKDF2KdfConfig.ITERATIONS.min), - Validators.max(PBKDF2KdfConfig.ITERATIONS.max), - ]; break; case KdfType.Argon2id: config = new Argon2KdfConfig(); - validators.iterations = [ - Validators.required, - Validators.min(Argon2KdfConfig.ITERATIONS.min), - Validators.max(Argon2KdfConfig.ITERATIONS.max), - ]; - validators.memory = [ - Validators.required, - Validators.min(Argon2KdfConfig.MEMORY.min), - Validators.max(Argon2KdfConfig.MEMORY.max), - ]; - validators.parallelism = [ - Validators.required, - Validators.min(Argon2KdfConfig.PARALLELISM.min), - Validators.max(Argon2KdfConfig.PARALLELISM.max), - ]; break; default: throw new Error("Unknown KDF type."); } this.kdfConfig = config; - this.setFormValidators(validators); + this.setFormValidators(newValue); this.setFormControlValues(this.kdfConfig); } - private setFormValidators(validators: { [key: string]: ValidatorFn[] }) { - this.setValidators("kdfConfig.iterations", validators.iterations); - this.setValidators("kdfConfig.memory", validators.memory); - this.setValidators("kdfConfig.parallelism", validators.parallelism); - } - private setValidators(controlName: string, validators: ValidatorFn[]) { - const control = this.formGroup.get(controlName); - if (control) { - control.setValidators(validators); - control.updateValueAndValidity(); + private setFormValidators(kdfType: KdfType) { + const kdfConfigFormGroup = this.formGroup.controls.kdfConfig; + switch (kdfType) { + case KdfType.PBKDF2_SHA256: + kdfConfigFormGroup.controls.iterations.setValidators([ + Validators.required, + Validators.min(PBKDF2KdfConfig.ITERATIONS.min), + Validators.max(PBKDF2KdfConfig.ITERATIONS.max), + ]); + kdfConfigFormGroup.controls.memory.setValidators([]); + kdfConfigFormGroup.controls.parallelism.setValidators([]); + break; + case KdfType.Argon2id: + kdfConfigFormGroup.controls.iterations.setValidators([ + Validators.required, + Validators.min(Argon2KdfConfig.ITERATIONS.min), + Validators.max(Argon2KdfConfig.ITERATIONS.max), + ]); + kdfConfigFormGroup.controls.memory.setValidators([ + Validators.required, + Validators.min(Argon2KdfConfig.MEMORY.min), + Validators.max(Argon2KdfConfig.MEMORY.max), + ]); + kdfConfigFormGroup.controls.parallelism.setValidators([ + Validators.required, + Validators.min(Argon2KdfConfig.PARALLELISM.min), + Validators.max(Argon2KdfConfig.PARALLELISM.max), + ]); + break; + default: + throw new Error("Unknown KDF type."); } + kdfConfigFormGroup.controls.iterations.updateValueAndValidity(); + kdfConfigFormGroup.controls.memory.updateValueAndValidity(); + kdfConfigFormGroup.controls.parallelism.updateValueAndValidity(); } + private setFormControlValues(kdfConfig: KdfConfig) { - this.formGroup.get("kdfConfig").reset(); + const kdfConfigFormGroup = this.formGroup.controls.kdfConfig; + kdfConfigFormGroup.reset(); if (kdfConfig.kdfType === KdfType.PBKDF2_SHA256) { - this.formGroup.get("kdfConfig.iterations").setValue(kdfConfig.iterations); + kdfConfigFormGroup.controls.iterations.setValue(kdfConfig.iterations); } else if (kdfConfig.kdfType === KdfType.Argon2id) { - this.formGroup.get("kdfConfig.iterations").setValue(kdfConfig.iterations); - this.formGroup.get("kdfConfig.memory").setValue(kdfConfig.memory); - this.formGroup.get("kdfConfig.parallelism").setValue(kdfConfig.parallelism); + kdfConfigFormGroup.controls.iterations.setValue(kdfConfig.iterations); + kdfConfigFormGroup.controls.memory.setValue(kdfConfig.memory); + kdfConfigFormGroup.controls.parallelism.setValue(kdfConfig.parallelism); } } @@ -155,12 +162,14 @@ export class ChangeKdfComponent implements OnInit, OnDestroy { if (this.formGroup.invalid) { return; } + + const kdfConfigFormGroup = this.formGroup.controls.kdfConfig; if (this.kdfConfig.kdfType === KdfType.PBKDF2_SHA256) { - this.kdfConfig.iterations = this.formGroup.get("kdfConfig.iterations").value; + this.kdfConfig.iterations = kdfConfigFormGroup.controls.iterations.value!; } else if (this.kdfConfig.kdfType === KdfType.Argon2id) { - this.kdfConfig.iterations = this.formGroup.get("kdfConfig.iterations").value; - this.kdfConfig.memory = this.formGroup.get("kdfConfig.memory").value; - this.kdfConfig.parallelism = this.formGroup.get("kdfConfig.parallelism").value; + this.kdfConfig.iterations = kdfConfigFormGroup.controls.iterations.value!; + this.kdfConfig.memory = kdfConfigFormGroup.controls.memory.value!; + this.kdfConfig.parallelism = kdfConfigFormGroup.controls.parallelism.value!; } this.dialogService.open(ChangeKdfConfirmationComponent, { data: { diff --git a/apps/web/src/app/key-management/change-kdf/change-kdf.module.ts b/apps/web/src/app/key-management/change-kdf/change-kdf.module.ts index 342ad43e368..4c9cd00e79d 100644 --- a/apps/web/src/app/key-management/change-kdf/change-kdf.module.ts +++ b/apps/web/src/app/key-management/change-kdf/change-kdf.module.ts @@ -1,13 +1,15 @@ import { CommonModule } from "@angular/common"; import { NgModule } from "@angular/core"; +import { PopoverModule } from "@bitwarden/components"; + import { SharedModule } from "../../shared"; import { ChangeKdfConfirmationComponent } from "./change-kdf-confirmation.component"; import { ChangeKdfComponent } from "./change-kdf.component"; @NgModule({ - imports: [CommonModule, SharedModule], + imports: [CommonModule, SharedModule, PopoverModule], declarations: [ChangeKdfComponent, ChangeKdfConfirmationComponent], exports: [ChangeKdfComponent, ChangeKdfConfirmationComponent], }) diff --git a/apps/web/src/app/vault/components/browser-extension-prompt/browser-extension-prompt.component.html b/apps/web/src/app/vault/components/browser-extension-prompt/browser-extension-prompt.component.html index 56332cc424b..aff549a84f4 100644 --- a/apps/web/src/app/vault/components/browser-extension-prompt/browser-extension-prompt.component.html +++ b/apps/web/src/app/vault/components/browser-extension-prompt/browser-extension-prompt.component.html @@ -4,13 +4,14 @@

{{ "openingExtension" | i18n }}

+ @let page = extensionPage$ | async;

{{ "openingExtensionError" | i18n }}