1
0
mirror of https://github.com/bitwarden/browser synced 2026-02-05 03:03:26 +00:00

Merge branch 'main' into km/replace-encstring-with-unsigned-shared-key

This commit is contained in:
Bernd Schoolmann
2025-12-12 18:34:10 +01:00
committed by GitHub
330 changed files with 8886 additions and 4437 deletions

View File

@@ -3,9 +3,7 @@ import { DeviceManagementComponentServiceAbstraction } from "./device-management
/**
* Default implementation of the device management component service
*/
export class DefaultDeviceManagementComponentService
implements DeviceManagementComponentServiceAbstraction
{
export class DefaultDeviceManagementComponentService implements DeviceManagementComponentServiceAbstraction {
/**
* Show header information in web client
*/

View File

@@ -5,11 +5,7 @@ import { MockProxy, mock } from "jest-mock-extended";
import { BehaviorSubject, of } from "rxjs";
import { EmptyComponent } from "@bitwarden/angular/platform/guard/feature-flag.guard.spec";
import {
Account,
AccountInfo,
AccountService,
} from "@bitwarden/common/auth/abstractions/account.service";
import { Account, AccountService } from "@bitwarden/common/auth/abstractions/account.service";
import { AuthService } from "@bitwarden/common/auth/abstractions/auth.service";
import { AuthenticationStatus } from "@bitwarden/common/auth/enums/authentication-status";
import { ForceSetPasswordReason } from "@bitwarden/common/auth/models/domain/force-set-password-reason";
@@ -18,6 +14,7 @@ import { KeyConnectorService } from "@bitwarden/common/key-management/key-connec
import { MasterPasswordServiceAbstraction } from "@bitwarden/common/key-management/master-password/abstractions/master-password.service.abstraction";
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service";
import { mockAccountInfoWith } from "@bitwarden/common/spec";
import { UserId } from "@bitwarden/common/types/guid";
import { authGuard } from "./auth.guard";
@@ -38,16 +35,13 @@ describe("AuthGuard", () => {
const accountService: MockProxy<AccountService> = mock<AccountService>();
const activeAccountSubject = new BehaviorSubject<Account | null>(null);
accountService.activeAccount$ = activeAccountSubject;
activeAccountSubject.next(
Object.assign(
{
name: "Test User 1",
email: "test@email.com",
emailVerified: true,
} as AccountInfo,
{ id: "test-id" as UserId },
),
);
activeAccountSubject.next({
id: "test-id" as UserId,
...mockAccountInfoWith({
name: "Test User 1",
email: "test@email.com",
}),
});
if (featureFlag) {
configService.getFeatureFlag.mockResolvedValue(true);

View File

@@ -5,11 +5,7 @@ import { MockProxy, mock } from "jest-mock-extended";
import { BehaviorSubject, of } from "rxjs";
import { EmptyComponent } from "@bitwarden/angular/platform/guard/feature-flag.guard.spec";
import {
Account,
AccountInfo,
AccountService,
} from "@bitwarden/common/auth/abstractions/account.service";
import { Account, AccountService } from "@bitwarden/common/auth/abstractions/account.service";
import { AuthService } from "@bitwarden/common/auth/abstractions/auth.service";
import { UserVerificationService } from "@bitwarden/common/auth/abstractions/user-verification/user-verification.service.abstraction";
import { AuthenticationStatus } from "@bitwarden/common/auth/enums/authentication-status";
@@ -20,6 +16,7 @@ import { KeyConnectorDomainConfirmation } from "@bitwarden/common/key-management
import { VaultTimeoutSettingsService } from "@bitwarden/common/key-management/vault-timeout";
import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service";
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
import { mockAccountInfoWith } from "@bitwarden/common/spec";
import { UserId } from "@bitwarden/common/types/guid";
import { KeyService } from "@bitwarden/key-management";
@@ -68,16 +65,13 @@ describe("lockGuard", () => {
const accountService: MockProxy<AccountService> = mock<AccountService>();
const activeAccountSubject = new BehaviorSubject<Account | null>(null);
accountService.activeAccount$ = activeAccountSubject;
activeAccountSubject.next(
Object.assign(
{
name: "Test User 1",
email: "test@email.com",
emailVerified: true,
} as AccountInfo,
{ id: "test-id" as UserId },
),
);
activeAccountSubject.next({
id: "test-id" as UserId,
...mockAccountInfoWith({
name: "Test User 1",
email: "test@email.com",
}),
});
const testBed = TestBed.configureTestingModule({
imports: [

View File

@@ -7,6 +7,7 @@ import { EmptyComponent } from "@bitwarden/angular/platform/guard/feature-flag.g
import { Account, AccountService } from "@bitwarden/common/auth/abstractions/account.service";
import { AuthService } from "@bitwarden/common/auth/abstractions/auth.service";
import { AuthenticationStatus } from "@bitwarden/common/auth/enums/authentication-status";
import { mockAccountInfoWith } from "@bitwarden/common/spec";
import { UserId } from "@bitwarden/common/types/guid";
import { redirectToVaultIfUnlockedGuard } from "./redirect-to-vault-if-unlocked.guard";
@@ -14,9 +15,10 @@ import { redirectToVaultIfUnlockedGuard } from "./redirect-to-vault-if-unlocked.
describe("redirectToVaultIfUnlockedGuard", () => {
const activeUser: Account = {
id: "userId" as UserId,
email: "test@email.com",
emailVerified: true,
name: "Test User",
...mockAccountInfoWith({
email: "test@email.com",
name: "Test User",
}),
};
const setup = (activeUser: Account | null, authStatus: AuthenticationStatus | null) => {

View File

@@ -9,6 +9,7 @@ import { AuthService } from "@bitwarden/common/auth/abstractions/auth.service";
import { AuthenticationStatus } from "@bitwarden/common/auth/enums/authentication-status";
import { DeviceTrustServiceAbstraction } from "@bitwarden/common/key-management/device-trust/abstractions/device-trust.service.abstraction";
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
import { mockAccountInfoWith } from "@bitwarden/common/spec";
import { UserId } from "@bitwarden/common/types/guid";
import { KeyService } from "@bitwarden/key-management";
@@ -17,9 +18,10 @@ import { tdeDecryptionRequiredGuard } from "./tde-decryption-required.guard";
describe("tdeDecryptionRequiredGuard", () => {
const activeUser: Account = {
id: "fake_user_id" as UserId,
email: "test@email.com",
emailVerified: true,
name: "Test User",
...mockAccountInfoWith({
email: "test@email.com",
name: "Test User",
}),
};
const setup = (

View File

@@ -10,6 +10,7 @@ import { AuthService } from "@bitwarden/common/auth/abstractions/auth.service";
import { AuthenticationStatus } from "@bitwarden/common/auth/enums/authentication-status";
import { DeviceTrustServiceAbstraction } from "@bitwarden/common/key-management/device-trust/abstractions/device-trust.service.abstraction";
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
import { mockAccountInfoWith } from "@bitwarden/common/spec";
import { UserId } from "@bitwarden/common/types/guid";
import { KeyService } from "@bitwarden/key-management";
@@ -18,9 +19,10 @@ import { unauthGuardFn } from "./unauth.guard";
describe("UnauthGuard", () => {
const activeUser: Account = {
id: "fake_user_id" as UserId,
email: "test@email.com",
emailVerified: true,
name: "Test User",
...mockAccountInfoWith({
email: "test@email.com",
name: "Test User",
}),
};
const setup = (

View File

@@ -3,9 +3,7 @@ import { LoginApprovalDialogComponentServiceAbstraction } from "./login-approval
/**
* Default implementation of the LoginApprovalDialogComponentServiceAbstraction.
*/
export class DefaultLoginApprovalDialogComponentService
implements LoginApprovalDialogComponentServiceAbstraction
{
export class DefaultLoginApprovalDialogComponentService implements LoginApprovalDialogComponentServiceAbstraction {
/**
* No-op implementation of the showLoginRequestedAlertIfWindowNotVisible method.
* @returns

View File

@@ -11,6 +11,7 @@ import { DevicesServiceAbstraction } from "@bitwarden/common/auth/abstractions/d
import { AuthRequestResponse } from "@bitwarden/common/auth/models/response/auth-request.response";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { ValidationService } from "@bitwarden/common/platform/abstractions/validation.service";
import { mockAccountInfoWith } from "@bitwarden/common/spec";
import { UserId } from "@bitwarden/common/types/guid";
import { DialogRef, DIALOG_DATA, ToastService } from "@bitwarden/components";
import { LogService } from "@bitwarden/logging";
@@ -48,10 +49,11 @@ describe("LoginApprovalDialogComponent", () => {
validationService = mock<ValidationService>();
accountService.activeAccount$ = of({
email: testEmail,
id: "test-user-id" as UserId,
emailVerified: true,
name: null,
...mockAccountInfoWith({
email: testEmail,
name: null,
}),
});
await TestBed.configureTestingModule({

View File

@@ -8,6 +8,7 @@ import { MasterPasswordApiService } from "@bitwarden/common/auth/abstractions/ma
import { EncString } from "@bitwarden/common/key-management/crypto/models/enc-string";
import { InternalMasterPasswordServiceAbstraction } from "@bitwarden/common/key-management/master-password/abstractions/master-password.service.abstraction";
import { SymmetricCryptoKey } from "@bitwarden/common/platform/models/domain/symmetric-crypto-key";
import { mockAccountInfoWith } from "@bitwarden/common/spec";
import { UserId } from "@bitwarden/common/types/guid";
import { MasterKey, UserKey } from "@bitwarden/common/types/key";
import { KeyService, PBKDF2KdfConfig } from "@bitwarden/key-management";
@@ -26,9 +27,11 @@ describe("DefaultChangePasswordService", () => {
const user: Account = {
id: userId,
email: "email",
emailVerified: false,
name: "name",
...mockAccountInfoWith({
email: "email",
name: "name",
emailVerified: false,
}),
};
const passwordInputResult: PasswordInputResult = {

View File

@@ -14,10 +14,11 @@ import { BadgeModule } from "@bitwarden/components";
type="button"
*appNotPremium
bitBadge
variant="success"
[variant]="'primary'"
class="!tw-text-primary-600 !tw-border-primary-600"
(click)="promptForPremium($event)"
>
{{ "premium" | i18n }}
<i class="bwi bwi-premium tw-pe-1"></i>{{ "upgrade" | i18n }}
</button>
`,
imports: [BadgeModule, JslibModule],

View File

@@ -29,7 +29,7 @@ export default {
provide: I18nService,
useFactory: () => {
return new I18nMockService({
premium: "Premium",
upgrade: "Upgrade",
});
},
},

View File

@@ -1,26 +0,0 @@
<bit-callout [icon]="icon" [title]="title" [type]="$any(type)" [useAlertRole]="useAlertRole">
<div class="tw-pl-7 tw-m-0" *ngIf="enforcedPolicyOptions">
{{ enforcedPolicyMessage }}
<ul>
<li *ngIf="enforcedPolicyOptions?.minComplexity > 0">
{{ "policyInEffectMinComplexity" | i18n: getPasswordScoreAlertDisplay() }}
</li>
<li *ngIf="enforcedPolicyOptions?.minLength > 0">
{{ "policyInEffectMinLength" | i18n: enforcedPolicyOptions?.minLength.toString() }}
</li>
<li *ngIf="enforcedPolicyOptions?.requireUpper">
{{ "policyInEffectUppercase" | i18n }}
</li>
<li *ngIf="enforcedPolicyOptions?.requireLower">
{{ "policyInEffectLowercase" | i18n }}
</li>
<li *ngIf="enforcedPolicyOptions?.requireNumbers">
{{ "policyInEffectNumbers" | i18n }}
</li>
<li *ngIf="enforcedPolicyOptions?.requireSpecial">
{{ "policyInEffectSpecial" | i18n: "!@#$%^&*" }}
</li>
</ul>
</div>
<ng-content></ng-content>
</bit-callout>

View File

@@ -1,70 +0,0 @@
// FIXME: Update this file to be type safe and remove this and next line
// @ts-strict-ignore
import { Component, Input, OnInit } from "@angular/core";
import { MasterPasswordPolicyOptions } from "@bitwarden/common/admin-console/models/domain/master-password-policy-options";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { CalloutTypes } from "@bitwarden/components";
/**
* @deprecated use the CL's `CalloutComponent` 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({
selector: "app-callout",
templateUrl: "callout.component.html",
standalone: false,
})
export class DeprecatedCalloutComponent implements OnInit {
// FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals
// eslint-disable-next-line @angular-eslint/prefer-signals
@Input() type: CalloutTypes = "info";
// FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals
// eslint-disable-next-line @angular-eslint/prefer-signals
@Input() icon: string;
// FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals
// eslint-disable-next-line @angular-eslint/prefer-signals
@Input() title: string;
// FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals
// eslint-disable-next-line @angular-eslint/prefer-signals
@Input() enforcedPolicyOptions: MasterPasswordPolicyOptions;
// FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals
// eslint-disable-next-line @angular-eslint/prefer-signals
@Input() enforcedPolicyMessage: string;
// FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals
// eslint-disable-next-line @angular-eslint/prefer-signals
@Input() useAlertRole = false;
calloutStyle: string;
constructor(private i18nService: I18nService) {}
ngOnInit() {
this.calloutStyle = this.type;
if (this.enforcedPolicyMessage === undefined) {
this.enforcedPolicyMessage = this.i18nService.t("masterPasswordPolicyInEffect");
}
}
getPasswordScoreAlertDisplay() {
if (this.enforcedPolicyOptions == null) {
return "";
}
let str: string;
switch (this.enforcedPolicyOptions.minComplexity) {
case 4:
str = this.i18nService.t("strong");
break;
case 3:
str = this.i18nService.t("good");
break;
default:
str = this.i18nService.t("weak");
break;
}
return str + " (" + this.enforcedPolicyOptions.minComplexity + ")";
}
}

View File

@@ -26,7 +26,6 @@ import {
import { TwoFactorIconComponent } from "./auth/components/two-factor-icon.component";
import { NotPremiumDirective } from "./billing/directives/not-premium.directive";
import { DeprecatedCalloutComponent } from "./components/callout.component";
import { A11yInvalidDirective } from "./directives/a11y-invalid.directive";
import { ApiActionDirective } from "./directives/api-action.directive";
import { BoxRowDirective } from "./directives/box-row.directive";
@@ -86,7 +85,6 @@ import { IconComponent } from "./vault/components/icon.component";
A11yInvalidDirective,
ApiActionDirective,
BoxRowDirective,
DeprecatedCalloutComponent,
CopyTextDirective,
CreditCardNumberPipe,
EllipsisPipe,
@@ -115,7 +113,6 @@ import { IconComponent } from "./vault/components/icon.component";
AutofocusDirective,
ToastModule,
BoxRowDirective,
DeprecatedCalloutComponent,
CopyTextDirective,
CreditCardNumberPipe,
EllipsisPipe,

View File

@@ -2,14 +2,13 @@ import { Router } from "@angular/router";
import { mock } from "jest-mock-extended";
import { of } from "rxjs";
import { AccountInfo } from "@bitwarden/common/auth/abstractions/account.service";
import { AuthService } from "@bitwarden/common/auth/abstractions/auth.service";
import { AuthenticationStatus } from "@bitwarden/common/auth/enums/authentication-status";
import { EncryptedMigrator } from "@bitwarden/common/key-management/encrypted-migrator/encrypted-migrator.abstraction";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { SingleUserState, StateProvider } from "@bitwarden/common/platform/state";
import { SyncService } from "@bitwarden/common/platform/sync";
import { FakeAccountService } from "@bitwarden/common/spec";
import { mockAccountInfoWith, FakeAccountService } from "@bitwarden/common/spec";
import { UserId } from "@bitwarden/common/types/guid";
import { DialogService, ToastService } from "@bitwarden/components";
import { LogService } from "@bitwarden/logging";
@@ -22,17 +21,15 @@ import { PromptMigrationPasswordComponent } from "./prompt-migration-password.co
const SomeUser = "SomeUser" as UserId;
const AnotherUser = "SomeOtherUser" as UserId;
const accounts: Record<UserId, AccountInfo> = {
[SomeUser]: {
const accounts = {
[SomeUser]: mockAccountInfoWith({
name: "some user",
email: "some.user@example.com",
emailVerified: true,
},
[AnotherUser]: {
}),
[AnotherUser]: mockAccountInfoWith({
name: "some other user",
email: "some.other.user@example.com",
emailVerified: true,
},
}),
};
describe("DefaultEncryptedMigrationsSchedulerService", () => {

View File

@@ -38,16 +38,14 @@ export const ENCRYPTED_MIGRATION_DISMISSED = new UserKeyDefinition<Date>(
},
);
const DISMISS_TIME_HOURS = 24;
const VAULT_ROUTE = "/vault";
const VAULT_ROUTES = ["/vault", "/tabs/vault", "/tabs/current"];
/**
* This services schedules encrypted migrations for users on clients that are interactive (non-cli), and handles manual interaction,
* if it is required by showing a UI prompt. It is only one means of triggering migrations, in case the user stays unlocked for a while,
* or regularly logs in without a master-password, when the migrations do require a master-password to run.
*/
export class DefaultEncryptedMigrationsSchedulerService
implements EncryptedMigrationsSchedulerService
{
export class DefaultEncryptedMigrationsSchedulerService implements EncryptedMigrationsSchedulerService {
isMigrating = false;
url$: Observable<string>;
@@ -87,7 +85,7 @@ export class DefaultEncryptedMigrationsSchedulerService
]).pipe(
filter(
([authStatus, _date, url]) =>
authStatus === AuthenticationStatus.Unlocked && url === VAULT_ROUTE,
authStatus === AuthenticationStatus.Unlocked && VAULT_ROUTES.includes(url),
),
concatMap(() => this.runMigrationsIfNeeded(userId)),
),

View File

@@ -184,7 +184,9 @@ import { DefaultChangeKdfApiService } from "@bitwarden/common/key-management/kdf
import { ChangeKdfApiService } from "@bitwarden/common/key-management/kdf/change-kdf-api.service.abstraction";
import { DefaultChangeKdfService } from "@bitwarden/common/key-management/kdf/change-kdf.service";
import { ChangeKdfService } from "@bitwarden/common/key-management/kdf/change-kdf.service.abstraction";
import { KeyConnectorApiService } from "@bitwarden/common/key-management/key-connector/abstractions/key-connector-api.service";
import { KeyConnectorService as KeyConnectorServiceAbstraction } from "@bitwarden/common/key-management/key-connector/abstractions/key-connector.service";
import { DefaultKeyConnectorApiService } from "@bitwarden/common/key-management/key-connector/services/default-key-connector-api.service";
import { KeyConnectorService } from "@bitwarden/common/key-management/key-connector/services/key-connector.service";
import { KeyApiService } from "@bitwarden/common/key-management/keys/services/abstractions/key-api-service.abstraction";
import { RotateableKeySetService } from "@bitwarden/common/key-management/keys/services/abstractions/rotateable-key-set.service";
@@ -950,7 +952,7 @@ const safeProviders: SafeProvider[] = [
deps: [
FolderServiceAbstraction,
CipherServiceAbstraction,
PinServiceAbstraction,
KeyGenerationService,
KeyService,
EncryptService,
CryptoFunctionServiceAbstraction,
@@ -970,7 +972,7 @@ const safeProviders: SafeProvider[] = [
deps: [
CipherServiceAbstraction,
VaultExportApiService,
PinServiceAbstraction,
KeyGenerationService,
KeyService,
EncryptService,
CryptoFunctionServiceAbstraction,
@@ -1355,16 +1357,7 @@ const safeProviders: SafeProvider[] = [
safeProvider({
provide: PinServiceAbstraction,
useClass: PinService,
deps: [
AccountServiceAbstraction,
EncryptService,
KdfConfigService,
KeyGenerationService,
LogService,
KeyService,
SdkService,
PinStateServiceAbstraction,
],
deps: [EncryptService, LogService, KeyService, SdkService, PinStateServiceAbstraction],
}),
safeProvider({
provide: WebAuthnLoginPrfKeyServiceAbstraction,
@@ -1835,6 +1828,11 @@ const safeProviders: SafeProvider[] = [
useClass: IpcSessionRepository,
deps: [StateProvider],
}),
safeProvider({
provide: KeyConnectorApiService,
useClass: DefaultKeyConnectorApiService,
deps: [ApiServiceAbstraction],
}),
safeProvider({
provide: PremiumInterestStateService,
useClass: NoopPremiumInterestStateService,

View File

@@ -1,9 +1,5 @@
<!-- Applying width and height styles directly to synchronize icon sizing between web/browser/desktop -->
<div
class="tw-flex tw-justify-center tw-items-center"
[ngStyle]="coloredIcon() ? { width: '36px', height: '36px' } : {}"
aria-hidden="true"
>
<div class="tw-flex tw-justify-center tw-items-center" [ngStyle]="iconStyle()" aria-hidden="true">
<ng-container *ngIf="data$ | async as data">
@if (data.imageEnabled && data.image) {
<img
@@ -16,7 +12,7 @@
'tw-invisible tw-absolute': !imageLoaded(),
'tw-size-6': !coloredIcon(),
}"
[ngStyle]="coloredIcon() ? { width: '36px', height: '36px' } : {}"
[ngStyle]="iconStyle()"
(load)="imageLoaded.set(true)"
(error)="imageLoaded.set(false)"
/>
@@ -28,7 +24,7 @@
'tw-bg-illustration-bg-primary tw-rounded-full':
data.icon?.startsWith('bwi-') && coloredIcon(),
}"
[ngStyle]="coloredIcon() ? { width: '36px', height: '36px' } : {}"
[ngStyle]="iconStyle()"
>
<i
class="tw-text-muted bwi bwi-lg {{ data.icon }}"
@@ -36,6 +32,7 @@
color: coloredIcon() ? 'rgb(var(--color-illustration-outline))' : null,
width: data.icon?.startsWith('credit-card') && coloredIcon() ? '36px' : null,
height: data.icon?.startsWith('credit-card') && coloredIcon() ? '30px' : null,
fontSize: size() ? size() + 'px' : null,
}"
></i>
</div>

View File

@@ -1,4 +1,4 @@
import { ChangeDetectionStrategy, Component, input, signal } from "@angular/core";
import { ChangeDetectionStrategy, Component, computed, input, signal } from "@angular/core";
import { toObservable } from "@angular/core/rxjs-interop";
import {
combineLatest,
@@ -32,8 +32,32 @@ export class IconComponent {
*/
readonly coloredIcon = input<boolean>(false);
/**
* Optional custom size for the icon in pixels.
* When provided, forces explicit dimensions on the icon wrapper to prevent layout collapse at different zoom levels.
* If not provided, the wrapper has no explicit dimensions and relies on CSS classes (tw-size-6/24px for images).
* This can cause the wrapper to collapse when images are loading/hidden, especially at high browser zoom levels.
* Reference: default image size is tw-size-6 (24px), coloredIcon uses 36px.
*/
readonly size = input<number>();
readonly imageLoaded = signal(false);
/**
* Computed style object for icon dimensions.
* Centralizes the sizing logic to avoid repetition in the template.
*/
protected readonly iconStyle = computed(() => {
if (this.coloredIcon()) {
return { width: "36px", height: "36px" };
}
const size = this.size();
if (size) {
return { width: size + "px", height: size + "px" };
}
return {};
});
protected data$: Observable<CipherIconDetails>;
constructor(