-
+
-
+
-
+
{
const mockOrganizationId = "mockOrgId" as OrganizationId;
@@ -112,5 +115,34 @@ describe("ImportService", () => {
]),
);
});
+
+ it("should generate user report export items and include users with no access", async () => {
+ reportApiService.getMemberAccessData.mockImplementation(() =>
+ Promise.resolve(memberAccessWithoutAccessDetailsReportsMock),
+ );
+ const result =
+ await memberAccessReportService.generateUserReportExportItems(mockOrganizationId);
+
+ expect(result).toEqual(
+ expect.arrayContaining([
+ expect.objectContaining({
+ email: "asmith@email.com",
+ name: "Alice Smith",
+ twoStepLogin: "memberAccessReportTwoFactorEnabledTrue",
+ accountRecovery: "memberAccessReportAuthenticationEnabledTrue",
+ group: "Alice Group 1",
+ totalItems: "10",
+ }),
+ expect.objectContaining({
+ email: "rbrown@email.com",
+ name: "Robert Brown",
+ twoStepLogin: "memberAccessReportTwoFactorEnabledFalse",
+ accountRecovery: "memberAccessReportAuthenticationEnabledFalse",
+ group: "memberAccessReportNoGroup",
+ totalItems: "0",
+ }),
+ ]),
+ );
+ });
});
});
diff --git a/bitwarden_license/bit-web/src/app/tools/reports/member-access-report/services/member-access-report.service.ts b/bitwarden_license/bit-web/src/app/tools/reports/member-access-report/services/member-access-report.service.ts
index b7ff5551e2c..029dce8a404 100644
--- a/bitwarden_license/bit-web/src/app/tools/reports/member-access-report/services/member-access-report.service.ts
+++ b/bitwarden_license/bit-web/src/app/tools/reports/member-access-report/services/member-access-report.service.ts
@@ -65,6 +65,26 @@ export class MemberAccessReportService {
}
const exportItems = memberAccessReports.flatMap((report) => {
+ // to include users without access details
+ // which means a user has no groups, collections or items
+ if (report.accessDetails.length === 0) {
+ return [
+ {
+ email: report.email,
+ name: report.userName,
+ twoStepLogin: report.twoFactorEnabled
+ ? this.i18nService.t("memberAccessReportTwoFactorEnabledTrue")
+ : this.i18nService.t("memberAccessReportTwoFactorEnabledFalse"),
+ accountRecovery: report.accountRecoveryEnabled
+ ? this.i18nService.t("memberAccessReportAuthenticationEnabledTrue")
+ : this.i18nService.t("memberAccessReportAuthenticationEnabledFalse"),
+ group: this.i18nService.t("memberAccessReportNoGroup"),
+ collection: this.i18nService.t("memberAccessReportNoCollection"),
+ collectionPermission: this.i18nService.t("memberAccessReportNoCollectionPermission"),
+ totalItems: "0",
+ },
+ ];
+ }
const userDetails = report.accessDetails.map((detail) => {
const collectionName = collectionNameMap.get(detail.collectionName.encryptedString);
return {
diff --git a/libs/angular/src/services/jslib-services.module.ts b/libs/angular/src/services/jslib-services.module.ts
index 3cce9b5357e..8e2b3409593 100644
--- a/libs/angular/src/services/jslib-services.module.ts
+++ b/libs/angular/src/services/jslib-services.module.ts
@@ -1255,6 +1255,7 @@ const safeProviders: SafeProvider[] = [
I18nServiceAbstraction,
OrganizationApiServiceAbstraction,
SyncService,
+ ConfigService,
],
}),
safeProvider({
diff --git a/libs/auth/src/angular/anon-layout/anon-layout.component.html b/libs/auth/src/angular/anon-layout/anon-layout.component.html
index f31a5500b43..1e16dba82cc 100644
--- a/libs/auth/src/angular/anon-layout/anon-layout.component.html
+++ b/libs/auth/src/angular/anon-layout/anon-layout.component.html
@@ -10,7 +10,7 @@
[routerLink]="['/']"
class="tw-w-[128px] tw-block tw-mb-12 [&>*]:tw-align-top"
>
-
+
{
});
it("should use the master key and hash if they exist", async () => {
- masterPasswordService.masterKeySubject.next({ encKey: new Uint8Array(64) } as MasterKey);
+ masterPasswordService.masterKeySubject.next(
+ new SymmetricCryptoKey(new Uint8Array(32)) as MasterKey,
+ );
masterPasswordService.masterKeyHashSubject.next("MASTER_KEY_HASH");
await sut.approveOrDenyAuthRequest(
@@ -115,7 +117,7 @@ describe("AuthRequestService", () => {
);
expect(encryptService.encapsulateKeyUnsigned).toHaveBeenCalledWith(
- { encKey: new Uint8Array(64) },
+ new SymmetricCryptoKey(new Uint8Array(32)),
expect.anything(),
);
});
diff --git a/libs/auth/src/common/services/auth-request/auth-request.service.ts b/libs/auth/src/common/services/auth-request/auth-request.service.ts
index f4316c2e519..226403d9c8c 100644
--- a/libs/auth/src/common/services/auth-request/auth-request.service.ts
+++ b/libs/auth/src/common/services/auth-request/auth-request.service.ts
@@ -14,6 +14,7 @@ import { AuthRequestPushNotification } from "@bitwarden/common/models/response/n
import { AppIdService } from "@bitwarden/common/platform/abstractions/app-id.service";
import { Utils } from "@bitwarden/common/platform/misc/utils";
import { EncString } from "@bitwarden/common/platform/models/domain/enc-string";
+import { SymmetricCryptoKey } from "@bitwarden/common/platform/models/domain/symmetric-crypto-key";
import {
AUTH_REQUEST_DISK_LOCAL,
StateProvider,
@@ -120,7 +121,10 @@ export class AuthRequestService implements AuthRequestServiceAbstraction {
keyToEncrypt = await this.keyService.getUserKey();
}
- const encryptedKey = await this.encryptService.encapsulateKeyUnsigned(keyToEncrypt, pubKey);
+ const encryptedKey = await this.encryptService.encapsulateKeyUnsigned(
+ keyToEncrypt as SymmetricCryptoKey,
+ pubKey,
+ );
const response = new PasswordlessAuthRequest(
encryptedKey.encryptedString,
diff --git a/libs/common/src/auth/abstractions/devices/responses/device.response.ts b/libs/common/src/auth/abstractions/devices/responses/device.response.ts
index 84a2fb03c28..6b7f17f65ce 100644
--- a/libs/common/src/auth/abstractions/devices/responses/device.response.ts
+++ b/libs/common/src/auth/abstractions/devices/responses/device.response.ts
@@ -15,6 +15,9 @@ export class DeviceResponse extends BaseResponse {
creationDate: string;
revisionDate: string;
isTrusted: boolean;
+ encryptedUserKey: string | null;
+ encryptedPublicKey: string | null;
+
devicePendingAuthRequest: DevicePendingAuthRequest | null;
constructor(response: any) {
@@ -27,6 +30,8 @@ export class DeviceResponse extends BaseResponse {
this.creationDate = this.getResponseProperty("CreationDate");
this.revisionDate = this.getResponseProperty("RevisionDate");
this.isTrusted = this.getResponseProperty("IsTrusted");
+ this.encryptedUserKey = this.getResponseProperty("EncryptedUserKey");
+ this.encryptedPublicKey = this.getResponseProperty("EncryptedPublicKey");
this.devicePendingAuthRequest = this.getResponseProperty("DevicePendingAuthRequest");
}
}
diff --git a/libs/common/src/billing/abstractions/organization-billing.service.ts b/libs/common/src/billing/abstractions/organization-billing.service.ts
index 69309014fac..8024a120b0a 100644
--- a/libs/common/src/billing/abstractions/organization-billing.service.ts
+++ b/libs/common/src/billing/abstractions/organization-billing.service.ts
@@ -1,5 +1,8 @@
// FIXME: Update this file to be type safe and remove this and next line
// @ts-strict-ignore
+import { Observable } from "rxjs";
+
+import { Organization } from "@bitwarden/common/admin-console/models/domain/organization";
import { OrganizationResponse } from "../../admin-console/models/response/organization.response";
import { InitiationPath } from "../../models/request/reference-event.request";
@@ -59,4 +62,10 @@ export abstract class OrganizationBillingServiceAbstraction {
organizationId: string,
subscription: SubscriptionInformation,
) => Promise
;
+
+ /**
+ * Determines if breadcrumbing policies is enabled for the organizations meeting certain criteria.
+ * @param organization
+ */
+ abstract isBreadcrumbingPoliciesEnabled$(organization: Organization): Observable;
}
diff --git a/libs/common/src/billing/services/organization-billing.service.spec.ts b/libs/common/src/billing/services/organization-billing.service.spec.ts
new file mode 100644
index 00000000000..7b194dff637
--- /dev/null
+++ b/libs/common/src/billing/services/organization-billing.service.spec.ts
@@ -0,0 +1,149 @@
+import { mock } from "jest-mock-extended";
+import { firstValueFrom, of } from "rxjs";
+
+import { ApiService } from "@bitwarden/common/abstractions/api.service";
+import { OrganizationApiServiceAbstraction as OrganizationApiService } from "@bitwarden/common/admin-console/abstractions/organization/organization-api.service.abstraction";
+import { Organization } from "@bitwarden/common/admin-console/models/domain/organization";
+import { BillingApiServiceAbstraction } from "@bitwarden/common/billing/abstractions";
+import { ProductTierType } from "@bitwarden/common/billing/enums";
+import { OrganizationBillingService } from "@bitwarden/common/billing/services/organization-billing.service";
+import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
+import { EncryptService } from "@bitwarden/common/key-management/crypto/abstractions/encrypt.service";
+import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
+import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
+import { SyncService } from "@bitwarden/common/platform/sync";
+import { KeyService } from "@bitwarden/key-management";
+
+describe("BillingAccountProfileStateService", () => {
+ let apiService: jest.Mocked;
+ let billingApiService: jest.Mocked;
+ let keyService: jest.Mocked;
+ let encryptService: jest.Mocked;
+ let i18nService: jest.Mocked;
+ let organizationApiService: jest.Mocked;
+ let syncService: jest.Mocked;
+ let configService: jest.Mocked;
+
+ let sut: OrganizationBillingService;
+
+ beforeEach(() => {
+ apiService = mock();
+ billingApiService = mock();
+ keyService = mock();
+ encryptService = mock();
+ i18nService = mock();
+ organizationApiService = mock();
+ syncService = mock();
+ configService = mock();
+
+ sut = new OrganizationBillingService(
+ apiService,
+ billingApiService,
+ keyService,
+ encryptService,
+ i18nService,
+ organizationApiService,
+ syncService,
+ configService,
+ );
+ });
+
+ afterEach(() => {
+ return jest.resetAllMocks();
+ });
+
+ describe("isBreadcrumbingPoliciesEnabled", () => {
+ it("returns false when feature flag is disabled", async () => {
+ configService.getFeatureFlag$.mockReturnValue(of(false));
+ const org = {
+ isProviderUser: false,
+ canEditSubscription: true,
+ productTierType: ProductTierType.Teams,
+ } as Organization;
+
+ const actual = await firstValueFrom(sut.isBreadcrumbingPoliciesEnabled$(org));
+ expect(actual).toBe(false);
+ expect(configService.getFeatureFlag$).toHaveBeenCalledWith(
+ FeatureFlag.PM12276_BreadcrumbEventLogs,
+ );
+ });
+
+ it("returns false when organization belongs to a provider", async () => {
+ configService.getFeatureFlag$.mockReturnValue(of(true));
+ const org = {
+ isProviderUser: true,
+ canEditSubscription: true,
+ productTierType: ProductTierType.Teams,
+ } as Organization;
+
+ const actual = await firstValueFrom(sut.isBreadcrumbingPoliciesEnabled$(org));
+ expect(actual).toBe(false);
+ });
+
+ it("returns false when cannot edit subscription", async () => {
+ configService.getFeatureFlag$.mockReturnValue(of(true));
+ const org = {
+ isProviderUser: false,
+ canEditSubscription: false,
+ productTierType: ProductTierType.Teams,
+ } as Organization;
+
+ const actual = await firstValueFrom(sut.isBreadcrumbingPoliciesEnabled$(org));
+ expect(actual).toBe(false);
+ });
+
+ it.each([
+ ["Teams", ProductTierType.Teams],
+ ["TeamsStarter", ProductTierType.TeamsStarter],
+ ])("returns true when all conditions are met with %s tier", async (_, productTierType) => {
+ configService.getFeatureFlag$.mockReturnValue(of(true));
+ const org = {
+ isProviderUser: false,
+ canEditSubscription: true,
+ productTierType: productTierType,
+ } as Organization;
+
+ const actual = await firstValueFrom(sut.isBreadcrumbingPoliciesEnabled$(org));
+ expect(actual).toBe(true);
+ expect(configService.getFeatureFlag$).toHaveBeenCalledWith(
+ FeatureFlag.PM12276_BreadcrumbEventLogs,
+ );
+ });
+
+ it("returns false when product tier is not supported", async () => {
+ configService.getFeatureFlag$.mockReturnValue(of(true));
+ const org = {
+ isProviderUser: false,
+ canEditSubscription: true,
+ productTierType: ProductTierType.Enterprise,
+ } as Organization;
+
+ const actual = await firstValueFrom(sut.isBreadcrumbingPoliciesEnabled$(org));
+ expect(actual).toBe(false);
+ });
+
+ it("handles all conditions false correctly", async () => {
+ configService.getFeatureFlag$.mockReturnValue(of(false));
+ const org = {
+ isProviderUser: true,
+ canEditSubscription: false,
+ productTierType: ProductTierType.Free,
+ } as Organization;
+
+ const actual = await firstValueFrom(sut.isBreadcrumbingPoliciesEnabled$(org));
+ expect(actual).toBe(false);
+ });
+
+ it("verifies feature flag is only called once", async () => {
+ configService.getFeatureFlag$.mockReturnValue(of(false));
+ const org = {
+ isProviderUser: false,
+ canEditSubscription: true,
+ productTierType: ProductTierType.Teams,
+ } as Organization;
+
+ await firstValueFrom(sut.isBreadcrumbingPoliciesEnabled$(org));
+ expect(configService.getFeatureFlag$).toHaveBeenCalledTimes(1);
+ });
+ });
+});
diff --git a/libs/common/src/billing/services/organization-billing.service.ts b/libs/common/src/billing/services/organization-billing.service.ts
index 83efbf0a30c..6622cdcdce3 100644
--- a/libs/common/src/billing/services/organization-billing.service.ts
+++ b/libs/common/src/billing/services/organization-billing.service.ts
@@ -1,5 +1,10 @@
// FIXME: Update this file to be type safe and remove this and next line
// @ts-strict-ignore
+import { Observable, of, switchMap } from "rxjs";
+
+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 { KeyService } from "@bitwarden/key-management";
import { ApiService } from "../../abstractions/api.service";
@@ -20,7 +25,7 @@ import {
PlanInformation,
SubscriptionInformation,
} from "../abstractions";
-import { PlanType } from "../enums";
+import { PlanType, ProductTierType } from "../enums";
import { OrganizationNoPaymentMethodCreateRequest } from "../models/request/organization-no-payment-method-create-request";
import { PaymentSourceResponse } from "../models/response/payment-source.response";
@@ -40,6 +45,7 @@ export class OrganizationBillingService implements OrganizationBillingServiceAbs
private i18nService: I18nService,
private organizationApiService: OrganizationApiService,
private syncService: SyncService,
+ private configService: ConfigService,
) {}
async getPaymentSource(organizationId: string): Promise {
@@ -220,4 +226,29 @@ export class OrganizationBillingService implements OrganizationBillingServiceAbs
this.setPaymentInformation(request, subscription.payment);
await this.billingApiService.restartSubscription(organizationId, request);
}
+
+ isBreadcrumbingPoliciesEnabled$(organization: Organization): Observable {
+ if (organization === null || organization === undefined) {
+ return of(false);
+ }
+
+ return this.configService.getFeatureFlag$(FeatureFlag.PM12276_BreadcrumbEventLogs).pipe(
+ switchMap((featureFlagEnabled) => {
+ if (!featureFlagEnabled) {
+ return of(false);
+ }
+
+ if (organization.isProviderUser || !organization.canEditSubscription) {
+ return of(false);
+ }
+
+ const supportedProducts = [ProductTierType.Teams, ProductTierType.TeamsStarter];
+ const isSupportedProduct = supportedProducts.some(
+ (product) => product === organization.productTierType,
+ );
+
+ return of(isSupportedProduct);
+ }),
+ );
+ }
}
diff --git a/libs/common/src/key-management/crypto/services/encrypt.service.implementation.ts b/libs/common/src/key-management/crypto/services/encrypt.service.implementation.ts
index 4b299c9c6e6..e9d8c1c30a2 100644
--- a/libs/common/src/key-management/crypto/services/encrypt.service.implementation.ts
+++ b/libs/common/src/key-management/crypto/services/encrypt.service.implementation.ts
@@ -47,7 +47,7 @@ export class EncryptServiceImplementation implements EncryptService {
}
if (this.blockType0) {
- if (key.encType === EncryptionType.AesCbc256_B64 || key.key.byteLength < 64) {
+ if (key.inner().type === EncryptionType.AesCbc256_B64 || key.key.byteLength < 64) {
throw new Error("Type 0 encryption is not supported.");
}
}
@@ -84,7 +84,7 @@ export class EncryptServiceImplementation implements EncryptService {
}
if (this.blockType0) {
- if (key.encType === EncryptionType.AesCbc256_B64 || key.key.byteLength < 64) {
+ if (key.inner().type === EncryptionType.AesCbc256_B64 || key.key.byteLength < 64) {
throw new Error("Type 0 encryption is not supported.");
}
}
@@ -124,7 +124,7 @@ export class EncryptServiceImplementation implements EncryptService {
if (encString.encryptionType !== innerKey.type) {
this.logDecryptError(
"Key encryption type does not match payload encryption type",
- key.encType,
+ innerKey.type,
encString.encryptionType,
decryptContext,
);
@@ -148,7 +148,7 @@ export class EncryptServiceImplementation implements EncryptService {
if (!macsEqual) {
this.logMacFailed(
"decryptToUtf8 MAC comparison failed. Key or payload has changed.",
- key.encType,
+ innerKey.type,
encString.encryptionType,
decryptContext,
);
@@ -191,7 +191,7 @@ export class EncryptServiceImplementation implements EncryptService {
if (encThing.encryptionType !== inner.type) {
this.logDecryptError(
"Encryption key type mismatch",
- key.encType,
+ inner.type,
encThing.encryptionType,
decryptContext,
);
@@ -200,19 +200,23 @@ export class EncryptServiceImplementation implements EncryptService {
if (inner.type === EncryptionType.AesCbc256_HmacSha256_B64) {
if (encThing.macBytes == null) {
- this.logDecryptError("Mac missing", key.encType, encThing.encryptionType, decryptContext);
+ this.logDecryptError("Mac missing", inner.type, encThing.encryptionType, decryptContext);
return null;
}
const macData = new Uint8Array(encThing.ivBytes.byteLength + encThing.dataBytes.byteLength);
macData.set(new Uint8Array(encThing.ivBytes), 0);
macData.set(new Uint8Array(encThing.dataBytes), encThing.ivBytes.byteLength);
- const computedMac = await this.cryptoFunctionService.hmac(macData, key.macKey, "sha256");
+ const computedMac = await this.cryptoFunctionService.hmac(
+ macData,
+ inner.authenticationKey,
+ "sha256",
+ );
const macsMatch = await this.cryptoFunctionService.compare(encThing.macBytes, computedMac);
if (!macsMatch) {
this.logMacFailed(
"MAC comparison failed. Key or payload has changed.",
- key.encType,
+ inner.type,
encThing.encryptionType,
decryptContext,
);
@@ -222,14 +226,14 @@ export class EncryptServiceImplementation implements EncryptService {
return await this.cryptoFunctionService.aesDecrypt(
encThing.dataBytes,
encThing.ivBytes,
- key.encKey,
+ inner.encryptionKey,
"cbc",
);
} else if (inner.type === EncryptionType.AesCbc256_B64) {
return await this.cryptoFunctionService.aesDecrypt(
encThing.dataBytes,
encThing.ivBytes,
- key.encKey,
+ inner.encryptionKey,
"cbc",
);
}
diff --git a/libs/common/src/key-management/crypto/services/encrypt.service.spec.ts b/libs/common/src/key-management/crypto/services/encrypt.service.spec.ts
index 4cbe3a3da90..fb55fa92605 100644
--- a/libs/common/src/key-management/crypto/services/encrypt.service.spec.ts
+++ b/libs/common/src/key-management/crypto/services/encrypt.service.spec.ts
@@ -6,7 +6,10 @@ import { EncryptionType } from "@bitwarden/common/platform/enums";
import { Utils } from "@bitwarden/common/platform/misc/utils";
import { EncArrayBuffer } from "@bitwarden/common/platform/models/domain/enc-array-buffer";
import { EncString } from "@bitwarden/common/platform/models/domain/enc-string";
-import { SymmetricCryptoKey } from "@bitwarden/common/platform/models/domain/symmetric-crypto-key";
+import {
+ Aes256CbcHmacKey,
+ SymmetricCryptoKey,
+} from "@bitwarden/common/platform/models/domain/symmetric-crypto-key";
import { CsprngArray } from "@bitwarden/common/types/csprng";
import { makeStaticByteArray } from "../../../../spec";
@@ -64,6 +67,10 @@ describe("EncryptService", () => {
const key = new SymmetricCryptoKey(makeStaticByteArray(32));
const mock32Key = mock();
mock32Key.key = makeStaticByteArray(32);
+ mock32Key.inner.mockReturnValue({
+ type: 0,
+ encryptionKey: mock32Key.key,
+ });
await expect(encryptService.encrypt(null!, key)).rejects.toThrow(
"Type 0 encryption is not supported.",
@@ -146,6 +153,10 @@ describe("EncryptService", () => {
const key = new SymmetricCryptoKey(makeStaticByteArray(32));
const mock32Key = mock();
mock32Key.key = makeStaticByteArray(32);
+ mock32Key.inner.mockReturnValue({
+ type: 0,
+ encryptionKey: mock32Key.key,
+ });
await expect(encryptService.encryptToBytes(plainValue, key)).rejects.toThrow(
"Type 0 encryption is not supported.",
@@ -228,7 +239,7 @@ describe("EncryptService", () => {
expect(cryptoFunctionService.aesDecrypt).toBeCalledWith(
expect.toEqualBuffer(encBuffer.dataBytes),
expect.toEqualBuffer(encBuffer.ivBytes),
- expect.toEqualBuffer(key.encKey),
+ expect.toEqualBuffer(key.inner().encryptionKey),
"cbc",
);
@@ -249,7 +260,7 @@ describe("EncryptService", () => {
expect(cryptoFunctionService.aesDecrypt).toBeCalledWith(
expect.toEqualBuffer(encBuffer.dataBytes),
expect.toEqualBuffer(encBuffer.ivBytes),
- expect.toEqualBuffer(key.encKey),
+ expect.toEqualBuffer(key.inner().encryptionKey),
"cbc",
);
@@ -267,7 +278,7 @@ describe("EncryptService", () => {
expect(cryptoFunctionService.hmac).toBeCalledWith(
expect.toEqualBuffer(expectedMacData),
- key.macKey,
+ (key.inner() as Aes256CbcHmacKey).authenticationKey,
"sha256",
);
diff --git a/libs/common/src/key-management/crypto/services/web-crypto-function.service.ts b/libs/common/src/key-management/crypto/services/web-crypto-function.service.ts
index 0c80d508b2d..430774ca2ed 100644
--- a/libs/common/src/key-management/crypto/services/web-crypto-function.service.ts
+++ b/libs/common/src/key-management/crypto/services/web-crypto-function.service.ts
@@ -1,6 +1,7 @@
import * as argon2 from "argon2-browser";
import * as forge from "node-forge";
+import { EncryptionType } from "../../../platform/enums";
import { Utils } from "../../../platform/misc/utils";
import {
CbcDecryptParameters,
@@ -247,37 +248,26 @@ export class WebCryptoFunctionService implements CryptoFunctionService {
mac: string | null,
key: SymmetricCryptoKey,
): CbcDecryptParameters {
- const p = {} as CbcDecryptParameters;
- if (key.meta != null) {
- p.encKey = key.meta.encKeyByteString;
- p.macKey = key.meta.macKeyByteString;
+ const innerKey = key.inner();
+ if (innerKey.type === EncryptionType.AesCbc256_B64) {
+ return {
+ iv: forge.util.decode64(iv),
+ data: forge.util.decode64(data),
+ encKey: forge.util.createBuffer(innerKey.encryptionKey).getBytes(),
+ } as CbcDecryptParameters;
+ } else if (innerKey.type === EncryptionType.AesCbc256_HmacSha256_B64) {
+ const macData = forge.util.decode64(iv) + forge.util.decode64(data);
+ return {
+ iv: forge.util.decode64(iv),
+ data: forge.util.decode64(data),
+ encKey: forge.util.createBuffer(innerKey.encryptionKey).getBytes(),
+ macKey: forge.util.createBuffer(innerKey.authenticationKey).getBytes(),
+ mac: forge.util.decode64(mac!),
+ macData,
+ } as CbcDecryptParameters;
+ } else {
+ throw new Error("Unsupported encryption type.");
}
-
- if (p.encKey == null) {
- p.encKey = forge.util.decode64(key.encKeyB64);
- }
- p.data = forge.util.decode64(data);
- p.iv = forge.util.decode64(iv);
- p.macData = p.iv + p.data;
- if (p.macKey == null && key.macKeyB64 != null) {
- p.macKey = forge.util.decode64(key.macKeyB64);
- }
- if (mac != null) {
- p.mac = forge.util.decode64(mac);
- }
-
- // cache byte string keys for later
- if (key.meta == null) {
- key.meta = {};
- }
- if (key.meta.encKeyByteString == null) {
- key.meta.encKeyByteString = p.encKey;
- }
- if (p.macKey != null && key.meta.macKeyByteString == null) {
- key.meta.macKeyByteString = p.macKey;
- }
-
- return p;
}
aesDecryptFast({
diff --git a/libs/common/src/key-management/device-trust/services/device-trust.service.implementation.ts b/libs/common/src/key-management/device-trust/services/device-trust.service.implementation.ts
index a2211753f4e..c82efa0c571 100644
--- a/libs/common/src/key-management/device-trust/services/device-trust.service.implementation.ts
+++ b/libs/common/src/key-management/device-trust/services/device-trust.service.implementation.ts
@@ -209,9 +209,8 @@ export class DeviceTrustService implements DeviceTrustServiceAbstraction {
devices.data
.filter((device) => device.isTrusted)
.map(async (device) => {
- const deviceWithKeys = await this.devicesApiService.getDeviceKeys(device.identifier);
const publicKey = await this.encryptService.decryptToBytes(
- deviceWithKeys.encryptedPublicKey,
+ new EncString(device.encryptedPublicKey),
oldUserKey,
);
diff --git a/libs/common/src/key-management/key-connector/services/key-connector.service.spec.ts b/libs/common/src/key-management/key-connector/services/key-connector.service.spec.ts
index fd3ce0c4777..b88ada56129 100644
--- a/libs/common/src/key-management/key-connector/services/key-connector.service.spec.ts
+++ b/libs/common/src/key-management/key-connector/services/key-connector.service.spec.ts
@@ -252,7 +252,9 @@ describe("KeyConnectorService", () => {
const organization = organizationData(true, true, "https://key-connector-url.com", 2, false);
const masterKey = getMockMasterKey();
masterPasswordService.masterKeySubject.next(masterKey);
- const keyConnectorRequest = new KeyConnectorUserKeyRequest(masterKey.encKeyB64);
+ const keyConnectorRequest = new KeyConnectorUserKeyRequest(
+ Utils.fromBufferToB64(masterKey.inner().encryptionKey),
+ );
jest.spyOn(keyConnectorService, "getManagingOrganization").mockResolvedValue(organization);
jest.spyOn(apiService, "postUserKeyToKeyConnector").mockResolvedValue();
@@ -273,7 +275,9 @@ describe("KeyConnectorService", () => {
// Arrange
const organization = organizationData(true, true, "https://key-connector-url.com", 2, false);
const masterKey = getMockMasterKey();
- const keyConnectorRequest = new KeyConnectorUserKeyRequest(masterKey.encKeyB64);
+ const keyConnectorRequest = new KeyConnectorUserKeyRequest(
+ Utils.fromBufferToB64(masterKey.inner().encryptionKey),
+ );
const error = new Error("Failed to post user key to key connector");
organizationService.organizations$.mockReturnValue(of([organization]));
diff --git a/libs/common/src/key-management/key-connector/services/key-connector.service.ts b/libs/common/src/key-management/key-connector/services/key-connector.service.ts
index 91b8e9100ac..9799f06f64a 100644
--- a/libs/common/src/key-management/key-connector/services/key-connector.service.ts
+++ b/libs/common/src/key-management/key-connector/services/key-connector.service.ts
@@ -95,7 +95,9 @@ export class KeyConnectorService implements KeyConnectorServiceAbstraction {
userId ??= (await firstValueFrom(this.accountService.activeAccount$))?.id;
const organization = await this.getManagingOrganization(userId);
const masterKey = await firstValueFrom(this.masterPasswordService.masterKey$(userId));
- const keyConnectorRequest = new KeyConnectorUserKeyRequest(masterKey.encKeyB64);
+ const keyConnectorRequest = new KeyConnectorUserKeyRequest(
+ Utils.fromBufferToB64(masterKey.inner().encryptionKey),
+ );
try {
await this.apiService.postUserKeyToKeyConnector(
@@ -157,7 +159,9 @@ export class KeyConnectorService implements KeyConnectorServiceAbstraction {
await this.tokenService.getEmail(),
kdfConfig,
);
- const keyConnectorRequest = new KeyConnectorUserKeyRequest(masterKey.encKeyB64);
+ const keyConnectorRequest = new KeyConnectorUserKeyRequest(
+ Utils.fromBufferToB64(masterKey.inner().encryptionKey),
+ );
await this.masterPasswordService.setMasterKey(masterKey, userId);
const userKey = await this.keyService.makeUserKey(masterKey);
diff --git a/libs/common/src/models/view/view.ts b/libs/common/src/models/view/view.ts
index 1f16b3d5958..2869617dca5 100644
--- a/libs/common/src/models/view/view.ts
+++ b/libs/common/src/models/view/view.ts
@@ -1 +1,5 @@
+// See https://contributing.bitwarden.com/architecture/clients/data-model/#view for proper use.
+// View models represent the decrypted state of a corresponding Domain model.
+// They typically match the Domain model but contains a decrypted string for any EncString fields.
+// Don't use this to represent arbitrary component view data as that isn't what it is for.
export class View {}
diff --git a/libs/common/src/platform/misc/utils.spec.ts b/libs/common/src/platform/misc/utils.spec.ts
index 964a2a19413..818138863fb 100644
--- a/libs/common/src/platform/misc/utils.spec.ts
+++ b/libs/common/src/platform/misc/utils.spec.ts
@@ -706,4 +706,73 @@ describe("Utils Service", () => {
});
});
});
+
+ describe("fromUtf8ToB64(...)", () => {
+ const originalIsNode = Utils.isNode;
+
+ afterEach(() => {
+ Utils.isNode = originalIsNode;
+ });
+
+ runInBothEnvironments("should handle empty string", () => {
+ const str = Utils.fromUtf8ToB64("");
+ expect(str).toBe("");
+ });
+
+ runInBothEnvironments("should convert a normal b64 string", () => {
+ const str = Utils.fromUtf8ToB64(asciiHelloWorld);
+ expect(str).toBe(b64HelloWorldString);
+ });
+
+ runInBothEnvironments("should convert various special characters", () => {
+ const cases = [
+ { input: "»", output: "wrs=" },
+ { input: "¦", output: "wqY=" },
+ { input: "£", output: "wqM=" },
+ { input: "é", output: "w6k=" },
+ { input: "ö", output: "w7Y=" },
+ { input: "»»", output: "wrvCuw==" },
+ ];
+ cases.forEach((c) => {
+ const utfStr = c.input;
+ const str = Utils.fromUtf8ToB64(utfStr);
+ expect(str).toBe(c.output);
+ });
+ });
+ });
+
+ describe("fromB64ToUtf8(...)", () => {
+ const originalIsNode = Utils.isNode;
+
+ afterEach(() => {
+ Utils.isNode = originalIsNode;
+ });
+
+ runInBothEnvironments("should handle empty string", () => {
+ const str = Utils.fromB64ToUtf8("");
+ expect(str).toBe("");
+ });
+
+ runInBothEnvironments("should convert a normal b64 string", () => {
+ const str = Utils.fromB64ToUtf8(b64HelloWorldString);
+ expect(str).toBe(asciiHelloWorld);
+ });
+
+ runInBothEnvironments("should handle various special characters", () => {
+ const cases = [
+ { input: "wrs=", output: "»" },
+ { input: "wqY=", output: "¦" },
+ { input: "wqM=", output: "£" },
+ { input: "w6k=", output: "é" },
+ { input: "w7Y=", output: "ö" },
+ { input: "wrvCuw==", output: "»»" },
+ ];
+
+ cases.forEach((c) => {
+ const b64Str = c.input;
+ const str = Utils.fromB64ToUtf8(b64Str);
+ expect(str).toBe(c.output);
+ });
+ });
+ });
});
diff --git a/libs/common/src/platform/misc/utils.ts b/libs/common/src/platform/misc/utils.ts
index ef65d2130a0..203a04851c5 100644
--- a/libs/common/src/platform/misc/utils.ts
+++ b/libs/common/src/platform/misc/utils.ts
@@ -233,7 +233,7 @@ export class Utils {
if (Utils.isNode) {
return Buffer.from(utfStr, "utf8").toString("base64");
} else {
- return decodeURIComponent(escape(Utils.global.btoa(utfStr)));
+ return BufferLib.from(utfStr, "utf8").toString("base64");
}
}
@@ -245,7 +245,7 @@ export class Utils {
if (Utils.isNode) {
return Buffer.from(b64Str, "base64").toString("utf8");
} else {
- return decodeURIComponent(escape(Utils.global.atob(b64Str)));
+ return BufferLib.from(b64Str, "base64").toString("utf8");
}
}
diff --git a/libs/common/src/platform/models/domain/symmetric-crypto-key.spec.ts b/libs/common/src/platform/models/domain/symmetric-crypto-key.spec.ts
index cce99b847bb..6b641ad443a 100644
--- a/libs/common/src/platform/models/domain/symmetric-crypto-key.spec.ts
+++ b/libs/common/src/platform/models/domain/symmetric-crypto-key.spec.ts
@@ -2,7 +2,7 @@ import { makeStaticByteArray } from "../../../../spec";
import { EncryptionType } from "../../enums";
import { Utils } from "../../misc/utils";
-import { SymmetricCryptoKey } from "./symmetric-crypto-key";
+import { Aes256CbcHmacKey, SymmetricCryptoKey } from "./symmetric-crypto-key";
describe("SymmetricCryptoKey", () => {
it("errors if no key", () => {
@@ -19,13 +19,8 @@ describe("SymmetricCryptoKey", () => {
const cryptoKey = new SymmetricCryptoKey(key);
expect(cryptoKey).toEqual({
- encKey: key,
- encKeyB64: "AAECAwQFBgcICQoLDA0ODxAREhMUFRYXGBkaGxwdHh8=",
- encType: EncryptionType.AesCbc256_B64,
key: key,
keyB64: "AAECAwQFBgcICQoLDA0ODxAREhMUFRYXGBkaGxwdHh8=",
- macKey: null,
- macKeyB64: undefined,
innerKey: {
type: EncryptionType.AesCbc256_B64,
encryptionKey: key,
@@ -38,14 +33,9 @@ describe("SymmetricCryptoKey", () => {
const cryptoKey = new SymmetricCryptoKey(key);
expect(cryptoKey).toEqual({
- encKey: key.slice(0, 32),
- encKeyB64: "AAECAwQFBgcICQoLDA0ODxAREhMUFRYXGBkaGxwdHh8=",
- encType: EncryptionType.AesCbc256_HmacSha256_B64,
key: key,
keyB64:
"AAECAwQFBgcICQoLDA0ODxAREhMUFRYXGBkaGxwdHh8gISIjJCUmJygpKissLS4vMDEyMzQ1Njc4OTo7PD0+Pw==",
- macKey: key.slice(32, 64),
- macKeyB64: "ICEiIyQlJicoKSorLC0uLzAxMjM0NTY3ODk6Ozw9Pj8=",
innerKey: {
type: EncryptionType.AesCbc256_HmacSha256_B64,
encryptionKey: key.slice(0, 32),
@@ -86,8 +76,8 @@ describe("SymmetricCryptoKey", () => {
expect(actual).toEqual({
type: EncryptionType.AesCbc256_HmacSha256_B64,
- encryptionKey: key.encKey,
- authenticationKey: key.macKey,
+ encryptionKey: key.inner().encryptionKey,
+ authenticationKey: (key.inner() as Aes256CbcHmacKey).authenticationKey,
});
});
@@ -95,7 +85,7 @@ describe("SymmetricCryptoKey", () => {
const key = new SymmetricCryptoKey(makeStaticByteArray(32));
const actual = key.toEncoded();
- expect(actual).toEqual(key.encKey);
+ expect(actual).toEqual(key.inner().encryptionKey);
});
it("toEncoded returns encoded key for AesCbc256_HmacSha256_B64", () => {
diff --git a/libs/common/src/platform/models/domain/symmetric-crypto-key.ts b/libs/common/src/platform/models/domain/symmetric-crypto-key.ts
index 45e15c1f602..c85f3432b28 100644
--- a/libs/common/src/platform/models/domain/symmetric-crypto-key.ts
+++ b/libs/common/src/platform/models/domain/symmetric-crypto-key.ts
@@ -25,15 +25,7 @@ export class SymmetricCryptoKey {
private innerKey: Aes256CbcHmacKey | Aes256CbcKey;
key: Uint8Array;
- encKey: Uint8Array;
- macKey?: Uint8Array;
- encType: EncryptionType;
-
keyB64: string;
- encKeyB64: string;
- macKeyB64: string;
-
- meta: any;
/**
* @param key The key in one of the permitted serialization formats
@@ -48,30 +40,16 @@ export class SymmetricCryptoKey {
type: EncryptionType.AesCbc256_B64,
encryptionKey: key,
};
- this.encType = EncryptionType.AesCbc256_B64;
this.key = key;
- this.keyB64 = Utils.fromBufferToB64(this.key);
-
- this.encKey = key;
- this.encKeyB64 = Utils.fromBufferToB64(this.encKey);
-
- this.macKey = null;
- this.macKeyB64 = undefined;
+ this.keyB64 = this.toBase64();
} else if (key.byteLength === 64) {
this.innerKey = {
type: EncryptionType.AesCbc256_HmacSha256_B64,
encryptionKey: key.slice(0, 32),
authenticationKey: key.slice(32),
};
- this.encType = EncryptionType.AesCbc256_HmacSha256_B64;
this.key = key;
- this.keyB64 = Utils.fromBufferToB64(this.key);
-
- this.encKey = key.slice(0, 32);
- this.encKeyB64 = Utils.fromBufferToB64(this.encKey);
-
- this.macKey = key.slice(32);
- this.macKeyB64 = Utils.fromBufferToB64(this.macKey);
+ this.keyB64 = this.toBase64();
} else {
throw new Error(`Unsupported encType/key length ${key.byteLength}`);
}
diff --git a/libs/common/src/platform/notifications/internal/worker-webpush-connection.service.ts b/libs/common/src/platform/notifications/internal/worker-webpush-connection.service.ts
index 74981b6782f..a1143d14d1d 100644
--- a/libs/common/src/platform/notifications/internal/worker-webpush-connection.service.ts
+++ b/libs/common/src/platform/notifications/internal/worker-webpush-connection.service.ts
@@ -134,7 +134,7 @@ class MyWebPushConnector implements WebPushConnector {
private async pushManagerSubscribe(key: string) {
return await this.serviceWorkerRegistration.pushManager.subscribe({
- userVisibleOnly: true,
+ userVisibleOnly: false,
applicationServerKey: key,
});
}
diff --git a/libs/components/src/icon/icon.component.ts b/libs/components/src/icon/icon.component.ts
index 2382d197bec..08fa25956d0 100644
--- a/libs/components/src/icon/icon.component.ts
+++ b/libs/components/src/icon/icon.component.ts
@@ -1,19 +1,23 @@
-// FIXME: Update this file to be type safe and remove this and next line
-// @ts-strict-ignore
-import { Component, HostBinding, Input } from "@angular/core";
+import { Component, Input } from "@angular/core";
import { DomSanitizer, SafeHtml } from "@angular/platform-browser";
import { Icon, isIcon } from "./icon";
@Component({
selector: "bit-icon",
+ host: {
+ "[attr.aria-hidden]": "!ariaLabel",
+ "[attr.aria-label]": "ariaLabel",
+ "[innerHtml]": "innerHtml",
+ },
template: ``,
standalone: true,
})
export class BitIconComponent {
+ innerHtml: SafeHtml | null = null;
+
@Input() set icon(icon: Icon) {
if (!isIcon(icon)) {
- this.innerHtml = "";
return;
}
@@ -21,7 +25,7 @@ export class BitIconComponent {
this.innerHtml = this.domSanitizer.bypassSecurityTrustHtml(svg);
}
- @HostBinding() innerHtml: SafeHtml;
+ @Input() ariaLabel: string | undefined = undefined;
constructor(private domSanitizer: DomSanitizer) {}
}
diff --git a/libs/components/src/icon/icon.mdx b/libs/components/src/icon/icon.mdx
index fc1c4cd3d57..6435fc24948 100644
--- a/libs/components/src/icon/icon.mdx
+++ b/libs/components/src/icon/icon.mdx
@@ -98,9 +98,19 @@ import * as stories from "./icon.stories";
```
- **HTML:**
+
+ > NOTE: SVG icons are treated as decorative by default and will be `aria-hidden` unless an
+ > `ariaLabel` is explicitly provided to the `` component
+
```html
```
+ With `ariaLabel`
+
+ ```html
+
+ ```
+
8. **Ensure your SVG renders properly** according to Figma in both light and dark modes on a client
which supports multiple style modes.
diff --git a/libs/components/src/icon/icon.stories.ts b/libs/components/src/icon/icon.stories.ts
index 53454567b7f..7892bdd3ec1 100644
--- a/libs/components/src/icon/icon.stories.ts
+++ b/libs/components/src/icon/icon.stories.ts
@@ -26,5 +26,9 @@ export const Default: Story = {
mapping: GenericIcons,
control: { type: "select" },
},
+ ariaLabel: {
+ control: "text",
+ description: "the text used by a screen reader to describe the icon",
+ },
},
};
diff --git a/libs/key-management-ui/src/index.ts b/libs/key-management-ui/src/index.ts
index 2f98538caad..b330e390d36 100644
--- a/libs/key-management-ui/src/index.ts
+++ b/libs/key-management-ui/src/index.ts
@@ -7,3 +7,4 @@ export { LockComponentService, UnlockOptions } from "./lock/services/lock-compon
export { KeyRotationTrustInfoComponent } from "./key-rotation/key-rotation-trust-info.component";
export { AccountRecoveryTrustComponent } from "./trust/account-recovery-trust.component";
export { EmergencyAccessTrustComponent } from "./trust/emergency-access-trust.component";
+export { RemovePasswordComponent } from "./key-connector/remove-password.component";
diff --git a/libs/angular/src/auth/components/remove-password.component.ts b/libs/key-management-ui/src/key-connector/remove-password.component.ts
similarity index 100%
rename from libs/angular/src/auth/components/remove-password.component.ts
rename to libs/key-management-ui/src/key-connector/remove-password.component.ts
diff --git a/libs/node/src/services/node-crypto-function.service.ts b/libs/node/src/services/node-crypto-function.service.ts
index 78d72d44104..33ea3adf357 100644
--- a/libs/node/src/services/node-crypto-function.service.ts
+++ b/libs/node/src/services/node-crypto-function.service.ts
@@ -3,6 +3,7 @@ import * as crypto from "crypto";
import * as forge from "node-forge";
import { CryptoFunctionService } from "@bitwarden/common/key-management/crypto/abstractions/crypto-function.service";
+import { EncryptionType } from "@bitwarden/common/platform/enums";
import { Utils } from "@bitwarden/common/platform/misc/utils";
import {
CbcDecryptParameters,
@@ -172,24 +173,33 @@ export class NodeCryptoFunctionService implements CryptoFunctionService {
mac: string | null,
key: SymmetricCryptoKey,
): CbcDecryptParameters {
- const p = {} as CbcDecryptParameters;
- p.encKey = key.encKey;
- p.data = Utils.fromB64ToArray(data);
- p.iv = Utils.fromB64ToArray(iv);
+ const dataBytes = Utils.fromB64ToArray(data);
+ const ivBytes = Utils.fromB64ToArray(iv);
+ const macBytes = mac != null ? Utils.fromB64ToArray(mac) : null;
- const macData = new Uint8Array(p.iv.byteLength + p.data.byteLength);
- macData.set(new Uint8Array(p.iv), 0);
- macData.set(new Uint8Array(p.data), p.iv.byteLength);
- p.macData = macData;
+ const innerKey = key.inner();
- if (key.macKey != null) {
- p.macKey = key.macKey;
+ if (innerKey.type === EncryptionType.AesCbc256_B64) {
+ return {
+ iv: ivBytes,
+ data: dataBytes,
+ encKey: innerKey.encryptionKey,
+ } as CbcDecryptParameters;
+ } else if (innerKey.type === EncryptionType.AesCbc256_HmacSha256_B64) {
+ const macData = new Uint8Array(ivBytes.byteLength + dataBytes.byteLength);
+ macData.set(new Uint8Array(ivBytes), 0);
+ macData.set(new Uint8Array(dataBytes), ivBytes.byteLength);
+ return {
+ iv: ivBytes,
+ data: dataBytes,
+ mac: macBytes,
+ macData: macData,
+ encKey: innerKey.encryptionKey,
+ macKey: innerKey.authenticationKey,
+ } as CbcDecryptParameters;
+ } else {
+ throw new Error("Unsupported encryption type");
}
- if (mac != null) {
- p.mac = Utils.fromB64ToArray(mac);
- }
-
- return p;
}
async aesDecryptFast({
diff --git a/libs/tools/export/vault-export/vault-export-ui/src/components/export.component.ts b/libs/tools/export/vault-export/vault-export-ui/src/components/export.component.ts
index 4e9b4175838..71599c19ae0 100644
--- a/libs/tools/export/vault-export/vault-export-ui/src/components/export.component.ts
+++ b/libs/tools/export/vault-export/vault-export-ui/src/components/export.component.ts
@@ -220,7 +220,7 @@ export class ExportComponent implements OnInit, OnDestroy, AfterViewInit {
this.exportForm.controls.vaultSelector.valueChanges
.pipe(takeUntil(this.destroy$))
- .subscribe(([value]) => {
+ .subscribe((value) => {
this.organizationId = value !== "myVault" ? value : undefined;
this.formatOptions = this.formatOptions.filter((option) => option.value !== "zip");
diff --git a/package-lock.json b/package-lock.json
index 65f0c87721a..3e16fd7ba68 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -191,11 +191,11 @@
},
"apps/browser": {
"name": "@bitwarden/browser",
- "version": "2025.3.2"
+ "version": "2025.4.0"
},
"apps/cli": {
"name": "@bitwarden/cli",
- "version": "2025.3.0",
+ "version": "2025.4.0",
"license": "SEE LICENSE IN LICENSE.txt",
"dependencies": {
"@koa/multer": "3.0.2",
@@ -231,7 +231,7 @@
},
"apps/desktop": {
"name": "@bitwarden/desktop",
- "version": "2025.4.1",
+ "version": "2025.4.2",
"hasInstallScript": true,
"license": "GPL-3.0"
},
@@ -245,7 +245,7 @@
},
"apps/web": {
"name": "@bitwarden/web-vault",
- "version": "2025.4.0"
+ "version": "2025.4.1"
},
"libs/admin-console": {
"name": "@bitwarden/admin-console",