From 385664c9771390f193dff91df7d815cb84ea679e Mon Sep 17 00:00:00 2001 From: Shane Melton Date: Wed, 15 May 2024 13:54:05 -0700 Subject: [PATCH 001/123] Add devFlag for the config service retrieval interval when developing (#9006) * Add devFlag for a configurable config retrieval interval when developing * Add Ms suffix to dev flag --- libs/common/src/platform/misc/flags.ts | 1 + .../services/config/default-config.service.ts | 11 +++++++---- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/libs/common/src/platform/misc/flags.ts b/libs/common/src/platform/misc/flags.ts index e0089a5451a..a77a06debb6 100644 --- a/libs/common/src/platform/misc/flags.ts +++ b/libs/common/src/platform/misc/flags.ts @@ -11,6 +11,7 @@ export type SharedFlags = { export type SharedDevFlags = { noopNotifications: boolean; skipWelcomeOnInstall: boolean; + configRetrievalIntervalMs: number; }; function getFlags(envFlags: string | T): T { diff --git a/libs/common/src/platform/services/config/default-config.service.ts b/libs/common/src/platform/services/config/default-config.service.ts index 71b76363a3b..0a306348d7b 100644 --- a/libs/common/src/platform/services/config/default-config.service.ts +++ b/libs/common/src/platform/services/config/default-config.service.ts @@ -1,13 +1,13 @@ import { - NEVER, - Observable, - Subject, combineLatest, firstValueFrom, map, mergeWith, + NEVER, + Observable, of, shareReplay, + Subject, switchMap, tap, } from "rxjs"; @@ -24,10 +24,13 @@ import { ConfigService } from "../../abstractions/config/config.service"; import { ServerConfig } from "../../abstractions/config/server-config"; import { EnvironmentService, Region } from "../../abstractions/environment.service"; import { LogService } from "../../abstractions/log.service"; +import { devFlagEnabled, devFlagValue } from "../../misc/flags"; import { ServerConfigData } from "../../models/data/server-config.data"; import { CONFIG_DISK, KeyDefinition, StateProvider, UserKeyDefinition } from "../../state"; -export const RETRIEVAL_INTERVAL = 3_600_000; // 1 hour +export const RETRIEVAL_INTERVAL = devFlagEnabled("configRetrievalIntervalMs") + ? (devFlagValue("configRetrievalIntervalMs") as number) + : 3_600_000; // 1 hour export type ApiUrl = string; From c19a640557f3b72e5259342f740acb8533b53693 Mon Sep 17 00:00:00 2001 From: Lorenzo Verardo Date: Wed, 15 May 2024 23:15:57 +0200 Subject: [PATCH 002/123] [PM-8059] Clarify warning message (#9141) --- .../change-kdf/change-kdf-confirmation.component.html | 2 +- .../settings/security/change-kdf/change-kdf.component.html | 2 +- apps/web/src/locales/en/messages.json | 4 ++-- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/apps/web/src/app/auth/settings/security/change-kdf/change-kdf-confirmation.component.html b/apps/web/src/app/auth/settings/security/change-kdf/change-kdf-confirmation.component.html index 139edd70e0f..8846104278c 100644 --- a/apps/web/src/app/auth/settings/security/change-kdf/change-kdf-confirmation.component.html +++ b/apps/web/src/app/auth/settings/security/change-kdf/change-kdf-confirmation.component.html @@ -4,7 +4,7 @@ - {{ "changeKdfLoggedOutWarning" | i18n }} + {{ "kdfSettingsChangeLogoutWarning" | i18n }}

{{ "encKeySettings" | i18n }}

-{{ "changeKdfLoggedOutWarning" | i18n }} +{{ "kdfSettingsChangeLogoutWarning" | i18n }}
diff --git a/apps/web/src/locales/en/messages.json b/apps/web/src/locales/en/messages.json index 3c96dd5df76..dd96824ac94 100644 --- a/apps/web/src/locales/en/messages.json +++ b/apps/web/src/locales/en/messages.json @@ -7014,8 +7014,8 @@ "updateLowKdfIterationsDesc": { "message": "Update your encryption settings to meet new security recommendations and improve account protection." }, - "changeKdfLoggedOutWarning": { - "message": "Proceeding will log you out of all active sessions. You will need to log back in and complete two-step login setup. We recommend exporting your vault before changing your encryption settings to prevent data loss." + "kdfSettingsChangeLogoutWarning": { + "message": "Proceeding will log you out of all active sessions. You will need to log back in and complete two-step login, if any. We recommend exporting your vault before changing your encryption settings to prevent data loss." }, "secretsManager": { "message": "Secrets Manager" From 4ccf920da8d373bcfa6c745fda8d27043ce421bd Mon Sep 17 00:00:00 2001 From: Matt Gibson Date: Wed, 15 May 2024 17:40:16 -0400 Subject: [PATCH 003/123] [PM-8155] Keep crypto derive dependencies in lockstep (#9191) * Keep derive dependencies in lockstep This reduces emissions in general due to updates of multiple inputs and removes decryption errors due to partially updated dependencies * Fix provider encrypted org keys * Fix provider state test types * Type fixes --- .../domain/encrypted-organization-key.ts | 38 +++++++++------ .../src/platform/services/crypto.service.ts | 47 ++++++++++--------- .../services/key-state/org-keys.state.spec.ts | 30 +++++++----- .../services/key-state/org-keys.state.ts | 35 +++++++++----- .../key-state/provider-keys.state.spec.ts | 7 +-- .../services/key-state/provider-keys.state.ts | 20 ++++---- .../services/key-state/user-key.state.spec.ts | 18 ++----- .../services/key-state/user-key.state.ts | 20 +++----- 8 files changed, 112 insertions(+), 103 deletions(-) diff --git a/libs/common/src/admin-console/models/domain/encrypted-organization-key.ts b/libs/common/src/admin-console/models/domain/encrypted-organization-key.ts index 470fa2317e6..1f8c4e8c42d 100644 --- a/libs/common/src/admin-console/models/domain/encrypted-organization-key.ts +++ b/libs/common/src/admin-console/models/domain/encrypted-organization-key.ts @@ -1,11 +1,11 @@ -import { CryptoService } from "../../../platform/abstractions/crypto.service"; +import { EncryptService } from "../../../platform/abstractions/encrypt.service"; import { EncString } from "../../../platform/models/domain/enc-string"; import { SymmetricCryptoKey } from "../../../platform/models/domain/symmetric-crypto-key"; -import { OrgKey } from "../../../types/key"; +import { OrgKey, UserPrivateKey } from "../../../types/key"; import { EncryptedOrganizationKeyData } from "../data/encrypted-organization-key.data"; export abstract class BaseEncryptedOrganizationKey { - decrypt: (cryptoService: CryptoService) => Promise; + abstract get encryptedOrganizationKey(): EncString; static fromData(data: EncryptedOrganizationKeyData) { switch (data.type) { @@ -19,22 +19,26 @@ export abstract class BaseEncryptedOrganizationKey { return null; } } + + static isProviderEncrypted( + key: EncryptedOrganizationKey | ProviderEncryptedOrganizationKey, + ): key is ProviderEncryptedOrganizationKey { + return key.toData().type === "provider"; + } } export class EncryptedOrganizationKey implements BaseEncryptedOrganizationKey { constructor(private key: string) {} - async decrypt(cryptoService: CryptoService) { - const activeUserPrivateKey = await cryptoService.getPrivateKey(); - - if (activeUserPrivateKey == null) { - throw new Error("Active user does not have a private key, cannot decrypt organization key."); - } - - const decValue = await cryptoService.rsaDecrypt(this.key, activeUserPrivateKey); + async decrypt(encryptService: EncryptService, privateKey: UserPrivateKey) { + const decValue = await encryptService.rsaDecrypt(this.encryptedOrganizationKey, privateKey); return new SymmetricCryptoKey(decValue) as OrgKey; } + get encryptedOrganizationKey() { + return new EncString(this.key); + } + toData(): EncryptedOrganizationKeyData { return { type: "organization", @@ -49,12 +53,18 @@ export class ProviderEncryptedOrganizationKey implements BaseEncryptedOrganizati private providerId: string, ) {} - async decrypt(cryptoService: CryptoService) { - const providerKey = await cryptoService.getProviderKey(this.providerId); - const decValue = await cryptoService.decryptToBytes(new EncString(this.key), providerKey); + async decrypt(encryptService: EncryptService, providerKeys: Record) { + const decValue = await encryptService.decryptToBytes( + new EncString(this.key), + providerKeys[this.providerId], + ); return new SymmetricCryptoKey(decValue) as OrgKey; } + get encryptedOrganizationKey() { + return new EncString(this.key); + } + toData(): EncryptedOrganizationKeyData { return { type: "provider", diff --git a/libs/common/src/platform/services/crypto.service.ts b/libs/common/src/platform/services/crypto.service.ts index fed22e06a02..2813bfb9608 100644 --- a/libs/common/src/platform/services/crypto.service.ts +++ b/libs/common/src/platform/services/crypto.service.ts @@ -1,5 +1,5 @@ import * as bigInt from "big-integer"; -import { Observable, filter, firstValueFrom, map } from "rxjs"; +import { Observable, filter, firstValueFrom, map, zip } from "rxjs"; import { PinServiceAbstraction } from "../../../../auth/src/common/abstractions"; import { EncryptedOrganizationKeyData } from "../../admin-console/models/data/encrypted-organization-key.data"; @@ -97,13 +97,12 @@ export class CryptoService implements CryptoServiceAbstraction { // User Asymmetric Key Pair this.activeUserEncryptedPrivateKeyState = stateProvider.getActive(USER_ENCRYPTED_PRIVATE_KEY); this.activeUserPrivateKeyState = stateProvider.getDerived( - this.activeUserEncryptedPrivateKeyState.combinedState$.pipe( - filter(([_userId, key]) => key != null), + zip(this.activeUserEncryptedPrivateKeyState.state$, this.activeUserKey$).pipe( + filter(([, userKey]) => !!userKey), ), USER_PRIVATE_KEY, { encryptService: this.encryptService, - getUserKey: (userId) => this.getUserKey(userId), }, ); this.activeUserPrivateKey$ = this.activeUserPrivateKeyState.state$; // may be null @@ -116,27 +115,34 @@ export class CryptoService implements CryptoServiceAbstraction { ); this.activeUserPublicKey$ = this.activeUserPublicKeyState.state$; // may be null - // Organization keys - this.activeUserEncryptedOrgKeysState = stateProvider.getActive( - USER_ENCRYPTED_ORGANIZATION_KEYS, - ); - this.activeUserOrgKeysState = stateProvider.getDerived( - this.activeUserEncryptedOrgKeysState.state$.pipe(filter((keys) => keys != null)), - USER_ORGANIZATION_KEYS, - { cryptoService: this }, - ); - this.activeUserOrgKeys$ = this.activeUserOrgKeysState.state$; // null handled by `derive` function - // Provider keys this.activeUserEncryptedProviderKeysState = stateProvider.getActive( USER_ENCRYPTED_PROVIDER_KEYS, ); this.activeUserProviderKeysState = stateProvider.getDerived( - this.activeUserEncryptedProviderKeysState.state$.pipe(filter((keys) => keys != null)), + zip( + this.activeUserEncryptedProviderKeysState.state$.pipe(filter((keys) => keys != null)), + this.activeUserPrivateKey$, + ).pipe(filter(([, privateKey]) => !!privateKey)), USER_PROVIDER_KEYS, - { encryptService: this.encryptService, cryptoService: this }, + { encryptService: this.encryptService }, ); this.activeUserProviderKeys$ = this.activeUserProviderKeysState.state$; // null handled by `derive` function + + // Organization keys + this.activeUserEncryptedOrgKeysState = stateProvider.getActive( + USER_ENCRYPTED_ORGANIZATION_KEYS, + ); + this.activeUserOrgKeysState = stateProvider.getDerived( + zip( + this.activeUserEncryptedOrgKeysState.state$.pipe(filter((keys) => keys != null)), + this.activeUserPrivateKey$, + this.activeUserProviderKeys$, + ).pipe(filter(([, privateKey]) => !!privateKey)), + USER_ORGANIZATION_KEYS, + { encryptService: this.encryptService }, + ); + this.activeUserOrgKeys$ = this.activeUserOrgKeysState.state$; // null handled by `derive` function } async setUserKey(key: UserKey, userId?: UserId): Promise { @@ -656,17 +662,14 @@ export class CryptoService implements CryptoServiceAbstraction { } try { - const [userId, encPrivateKey] = await firstValueFrom( - this.activeUserEncryptedPrivateKeyState.combinedState$, - ); + const encPrivateKey = await firstValueFrom(this.activeUserEncryptedPrivateKeyState.state$); if (encPrivateKey == null) { return false; } // Can decrypt private key - const privateKey = await USER_PRIVATE_KEY.derive([userId, encPrivateKey], { + const privateKey = await USER_PRIVATE_KEY.derive([encPrivateKey, key], { encryptService: this.encryptService, - getUserKey: () => Promise.resolve(key), }); if (privateKey == null) { diff --git a/libs/common/src/platform/services/key-state/org-keys.state.spec.ts b/libs/common/src/platform/services/key-state/org-keys.state.spec.ts index 6b547a491aa..98e0139cc4d 100644 --- a/libs/common/src/platform/services/key-state/org-keys.state.spec.ts +++ b/libs/common/src/platform/services/key-state/org-keys.state.spec.ts @@ -1,8 +1,8 @@ import { mock } from "jest-mock-extended"; import { makeEncString, makeStaticByteArray } from "../../../../spec"; -import { OrgKey } from "../../../types/key"; -import { CryptoService } from "../../abstractions/crypto.service"; +import { OrgKey, UserPrivateKey } from "../../../types/key"; +import { EncryptService } from "../../abstractions/encrypt.service"; import { SymmetricCryptoKey } from "../../models/domain/symmetric-crypto-key"; import { USER_ENCRYPTED_ORGANIZATION_KEYS, USER_ORGANIZATION_KEYS } from "./org-keys.state"; @@ -30,7 +30,8 @@ describe("encrypted org keys", () => { }); describe("derived decrypted org keys", () => { - const cryptoService = mock(); + const encryptService = mock(); + const userPrivateKey = makeStaticByteArray(64, 3) as UserPrivateKey; const sut = USER_ORGANIZATION_KEYS; afterEach(() => { @@ -65,15 +66,11 @@ describe("derived decrypted org keys", () => { "org-id-2": new SymmetricCryptoKey(makeStaticByteArray(64, 2)) as OrgKey, }; - const userPrivateKey = makeStaticByteArray(64, 3); - - cryptoService.getPrivateKey.mockResolvedValue(userPrivateKey); - // TODO: How to not have to mock these decryptions. They are internal concerns of EncryptedOrganizationKey - cryptoService.rsaDecrypt.mockResolvedValueOnce(decryptedOrgKeys["org-id-1"].key); - cryptoService.rsaDecrypt.mockResolvedValueOnce(decryptedOrgKeys["org-id-2"].key); + encryptService.rsaDecrypt.mockResolvedValueOnce(decryptedOrgKeys["org-id-1"].key); + encryptService.rsaDecrypt.mockResolvedValueOnce(decryptedOrgKeys["org-id-2"].key); - const result = await sut.derive(encryptedOrgKeys, { cryptoService }); + const result = await sut.derive([encryptedOrgKeys, userPrivateKey, {}], { encryptService }); expect(result).toEqual(decryptedOrgKeys); }); @@ -92,16 +89,23 @@ describe("derived decrypted org keys", () => { }, }; + const providerKeys = { + "provider-id-1": new SymmetricCryptoKey(makeStaticByteArray(64, 1)), + "provider-id-2": new SymmetricCryptoKey(makeStaticByteArray(64, 2)), + }; + const decryptedOrgKeys = { "org-id-1": new SymmetricCryptoKey(makeStaticByteArray(64, 1)) as OrgKey, "org-id-2": new SymmetricCryptoKey(makeStaticByteArray(64, 2)) as OrgKey, }; // TODO: How to not have to mock these decryptions. They are internal concerns of ProviderEncryptedOrganizationKey - cryptoService.decryptToBytes.mockResolvedValueOnce(decryptedOrgKeys["org-id-1"].key); - cryptoService.decryptToBytes.mockResolvedValueOnce(decryptedOrgKeys["org-id-2"].key); + encryptService.decryptToBytes.mockResolvedValueOnce(decryptedOrgKeys["org-id-1"].key); + encryptService.decryptToBytes.mockResolvedValueOnce(decryptedOrgKeys["org-id-2"].key); - const result = await sut.derive(encryptedOrgKeys, { cryptoService }); + const result = await sut.derive([encryptedOrgKeys, userPrivateKey, providerKeys], { + encryptService, + }); expect(result).toEqual(decryptedOrgKeys); }); diff --git a/libs/common/src/platform/services/key-state/org-keys.state.ts b/libs/common/src/platform/services/key-state/org-keys.state.ts index f67e64b6538..8a42e242b12 100644 --- a/libs/common/src/platform/services/key-state/org-keys.state.ts +++ b/libs/common/src/platform/services/key-state/org-keys.state.ts @@ -1,10 +1,10 @@ import { EncryptedOrganizationKeyData } from "../../../admin-console/models/data/encrypted-organization-key.data"; import { BaseEncryptedOrganizationKey } from "../../../admin-console/models/domain/encrypted-organization-key"; -import { OrganizationId } from "../../../types/guid"; -import { OrgKey } from "../../../types/key"; -import { CryptoService } from "../../abstractions/crypto.service"; +import { OrganizationId, ProviderId } from "../../../types/guid"; +import { OrgKey, ProviderKey, UserPrivateKey } from "../../../types/key"; +import { EncryptService } from "../../abstractions/encrypt.service"; import { SymmetricCryptoKey } from "../../models/domain/symmetric-crypto-key"; -import { CRYPTO_DISK, DeriveDefinition, UserKeyDefinition } from "../../state"; +import { CRYPTO_DISK, CRYPTO_MEMORY, DeriveDefinition, UserKeyDefinition } from "../../state"; export const USER_ENCRYPTED_ORGANIZATION_KEYS = UserKeyDefinition.record< EncryptedOrganizationKeyData, @@ -14,11 +14,15 @@ export const USER_ENCRYPTED_ORGANIZATION_KEYS = UserKeyDefinition.record< clearOn: ["logout"], }); -export const USER_ORGANIZATION_KEYS = DeriveDefinition.from< - Record, +export const USER_ORGANIZATION_KEYS = new DeriveDefinition< + [ + Record, + UserPrivateKey, + Record, + ], Record, - { cryptoService: CryptoService } ->(USER_ENCRYPTED_ORGANIZATION_KEYS, { + { encryptService: EncryptService } +>(CRYPTO_MEMORY, "organizationKeys", { deserializer: (obj) => { const result: Record = {}; for (const orgId of Object.keys(obj ?? {}) as OrganizationId[]) { @@ -26,14 +30,21 @@ export const USER_ORGANIZATION_KEYS = DeriveDefinition.from< } return result; }, - derive: async (from, { cryptoService }) => { + derive: async ([encryptedOrgKeys, privateKey, providerKeys], { encryptService }) => { const result: Record = {}; - for (const orgId of Object.keys(from ?? {}) as OrganizationId[]) { + for (const orgId of Object.keys(encryptedOrgKeys ?? {}) as OrganizationId[]) { if (result[orgId] != null) { continue; } - const encrypted = BaseEncryptedOrganizationKey.fromData(from[orgId]); - const decrypted = await encrypted.decrypt(cryptoService); + const encrypted = BaseEncryptedOrganizationKey.fromData(encryptedOrgKeys[orgId]); + + let decrypted: OrgKey; + + if (BaseEncryptedOrganizationKey.isProviderEncrypted(encrypted)) { + decrypted = await encrypted.decrypt(encryptService, providerKeys); + } else { + decrypted = await encrypted.decrypt(encryptService, privateKey); + } result[orgId] = decrypted; } diff --git a/libs/common/src/platform/services/key-state/provider-keys.state.spec.ts b/libs/common/src/platform/services/key-state/provider-keys.state.spec.ts index 78e61e03914..ca84d4a6ea1 100644 --- a/libs/common/src/platform/services/key-state/provider-keys.state.spec.ts +++ b/libs/common/src/platform/services/key-state/provider-keys.state.spec.ts @@ -6,7 +6,6 @@ import { ProviderKey, UserPrivateKey } from "../../../types/key"; import { EncryptService } from "../../abstractions/encrypt.service"; import { EncryptedString } from "../../models/domain/enc-string"; import { SymmetricCryptoKey } from "../../models/domain/symmetric-crypto-key"; -import { CryptoService } from "../crypto.service"; import { USER_ENCRYPTED_PROVIDER_KEYS, USER_PROVIDER_KEYS } from "./provider-keys.state"; @@ -27,7 +26,6 @@ describe("encrypted provider keys", () => { describe("derived decrypted provider keys", () => { const encryptService = mock(); - const cryptoService = mock(); const userPrivateKey = makeStaticByteArray(64, 0) as UserPrivateKey; const sut = USER_PROVIDER_KEYS; @@ -59,9 +57,8 @@ describe("derived decrypted provider keys", () => { encryptService.rsaDecrypt.mockResolvedValueOnce(decryptedProviderKeys["provider-id-1"].key); encryptService.rsaDecrypt.mockResolvedValueOnce(decryptedProviderKeys["provider-id-2"].key); - cryptoService.getPrivateKey.mockResolvedValueOnce(userPrivateKey); - const result = await sut.derive(encryptedProviderKeys, { encryptService, cryptoService }); + const result = await sut.derive([encryptedProviderKeys, userPrivateKey], { encryptService }); expect(result).toEqual(decryptedProviderKeys); }); @@ -69,7 +66,7 @@ describe("derived decrypted provider keys", () => { it("should handle null input values", async () => { const encryptedProviderKeys: Record = null; - const result = await sut.derive(encryptedProviderKeys, { encryptService, cryptoService }); + const result = await sut.derive([encryptedProviderKeys, userPrivateKey], { encryptService }); expect(result).toEqual({}); }); diff --git a/libs/common/src/platform/services/key-state/provider-keys.state.ts b/libs/common/src/platform/services/key-state/provider-keys.state.ts index 776fdc77d8b..dfda71be213 100644 --- a/libs/common/src/platform/services/key-state/provider-keys.state.ts +++ b/libs/common/src/platform/services/key-state/provider-keys.state.ts @@ -1,10 +1,9 @@ import { ProviderId } from "../../../types/guid"; -import { ProviderKey } from "../../../types/key"; +import { ProviderKey, UserPrivateKey } from "../../../types/key"; import { EncryptService } from "../../abstractions/encrypt.service"; import { EncString, EncryptedString } from "../../models/domain/enc-string"; import { SymmetricCryptoKey } from "../../models/domain/symmetric-crypto-key"; -import { CRYPTO_DISK, DeriveDefinition, UserKeyDefinition } from "../../state"; -import { CryptoService } from "../crypto.service"; +import { CRYPTO_DISK, CRYPTO_MEMORY, DeriveDefinition, UserKeyDefinition } from "../../state"; export const USER_ENCRYPTED_PROVIDER_KEYS = UserKeyDefinition.record( CRYPTO_DISK, @@ -15,11 +14,11 @@ export const USER_ENCRYPTED_PROVIDER_KEYS = UserKeyDefinition.record, +export const USER_PROVIDER_KEYS = new DeriveDefinition< + [Record, UserPrivateKey], Record, - { encryptService: EncryptService; cryptoService: CryptoService } // TODO: This should depend on an active user private key observable directly ->(USER_ENCRYPTED_PROVIDER_KEYS, { + { encryptService: EncryptService } +>(CRYPTO_MEMORY, "providerKeys", { deserializer: (obj) => { const result: Record = {}; for (const providerId of Object.keys(obj ?? {}) as ProviderId[]) { @@ -27,14 +26,13 @@ export const USER_PROVIDER_KEYS = DeriveDefinition.from< } return result; }, - derive: async (from, { encryptService, cryptoService }) => { + derive: async ([encryptedProviderKeys, privateKey], { encryptService }) => { const result: Record = {}; - for (const providerId of Object.keys(from ?? {}) as ProviderId[]) { + for (const providerId of Object.keys(encryptedProviderKeys ?? {}) as ProviderId[]) { if (result[providerId] != null) { continue; } - const encrypted = new EncString(from[providerId]); - const privateKey = await cryptoService.getPrivateKey(); + const encrypted = new EncString(encryptedProviderKeys[providerId]); const decrypted = await encryptService.rsaDecrypt(encrypted, privateKey); const providerKey = new SymmetricCryptoKey(decrypted) as ProviderKey; diff --git a/libs/common/src/platform/services/key-state/user-key.state.spec.ts b/libs/common/src/platform/services/key-state/user-key.state.spec.ts index 5c5c5ac70c5..63273f1c795 100644 --- a/libs/common/src/platform/services/key-state/user-key.state.spec.ts +++ b/libs/common/src/platform/services/key-state/user-key.state.spec.ts @@ -1,7 +1,6 @@ import { mock } from "jest-mock-extended"; import { makeStaticByteArray } from "../../../../spec"; -import { UserId } from "../../../types/guid"; import { UserKey, UserPrivateKey, UserPublicKey } from "../../../types/key"; import { CryptoFunctionService } from "../../abstractions/crypto-function.service"; import { EncryptService } from "../../abstractions/encrypt.service"; @@ -70,7 +69,6 @@ describe("User public key", () => { describe("Derived decrypted private key", () => { const sut = USER_PRIVATE_KEY; - const userId = "userId" as UserId; const userKey = mock(); const encryptedPrivateKey = makeEncString().encryptedString; const decryptedPrivateKey = makeStaticByteArray(64, 1); @@ -88,37 +86,31 @@ describe("Derived decrypted private key", () => { }); it("should derive decrypted private key", async () => { - const getUserKey = jest.fn(async () => userKey); const encryptService = mock(); encryptService.decryptToBytes.mockResolvedValue(decryptedPrivateKey); - const result = await sut.derive([userId, encryptedPrivateKey], { + const result = await sut.derive([encryptedPrivateKey, userKey], { encryptService, - getUserKey, }); expect(result).toEqual(decryptedPrivateKey); }); - it("should handle null input values", async () => { - const getUserKey = jest.fn(async () => userKey); + it("should handle null encryptedPrivateKey", async () => { const encryptService = mock(); - const result = await sut.derive([userId, null], { + const result = await sut.derive([null, userKey], { encryptService, - getUserKey, }); expect(result).toEqual(null); }); - it("should handle null user key", async () => { - const getUserKey = jest.fn(async () => null); + it("should handle null userKey", async () => { const encryptService = mock(); - const result = await sut.derive([userId, encryptedPrivateKey], { + const result = await sut.derive([encryptedPrivateKey, null], { encryptService, - getUserKey, }); expect(result).toEqual(null); diff --git a/libs/common/src/platform/services/key-state/user-key.state.ts b/libs/common/src/platform/services/key-state/user-key.state.ts index 3df3b2044bd..c2b84d6a247 100644 --- a/libs/common/src/platform/services/key-state/user-key.state.ts +++ b/libs/common/src/platform/services/key-state/user-key.state.ts @@ -1,4 +1,3 @@ -import { UserId } from "../../../types/guid"; import { UserPrivateKey, UserPublicKey, UserKey } from "../../../types/key"; import { CryptoFunctionService } from "../../abstractions/crypto-function.service"; import { EncryptService } from "../../abstractions/encrypt.service"; @@ -24,20 +23,14 @@ export const USER_ENCRYPTED_PRIVATE_KEY = new UserKeyDefinition }, ); -export const USER_PRIVATE_KEY = DeriveDefinition.fromWithUserId< - EncryptedString, +export const USER_PRIVATE_KEY = new DeriveDefinition< + [EncryptedString, UserKey], UserPrivateKey, - // TODO: update cryptoService to user key directly - { encryptService: EncryptService; getUserKey: (userId: UserId) => Promise } ->(USER_ENCRYPTED_PRIVATE_KEY, { + { encryptService: EncryptService } +>(CRYPTO_MEMORY, "privateKey", { deserializer: (obj) => new Uint8Array(Object.values(obj)) as UserPrivateKey, - derive: async ([userId, encPrivateKeyString], { encryptService, getUserKey }) => { - if (encPrivateKeyString == null) { - return null; - } - - const userKey = await getUserKey(userId); - if (userKey == null) { + derive: async ([encPrivateKeyString, userKey], { encryptService }) => { + if (encPrivateKeyString == null || userKey == null) { return null; } @@ -64,6 +57,7 @@ export const USER_PUBLIC_KEY = DeriveDefinition.from< return (await cryptoFunctionService.rsaExtractPublicKey(privateKey)) as UserPublicKey; }, }); + export const USER_KEY = new UserKeyDefinition(CRYPTO_MEMORY, "userKey", { deserializer: (obj) => SymmetricCryptoKey.fromJSON(obj) as UserKey, clearOn: ["logout", "lock"], From e55e3d5b9bf055a4dcae4d6b139295a26409f11c Mon Sep 17 00:00:00 2001 From: Matt Gibson Date: Wed, 15 May 2024 17:41:04 -0400 Subject: [PATCH 004/123] [PM-8159] [PM-8158] [PM-8156] Swallow multiple offscreen document errors (#9195) * Swallow multiple offscreen document errors The API has race issues with determining if an offscreen document exists (https://groups.google.com/a/chromium.org/g/chromium-extensions/c/s2Wp55bjySE/m/SnjJu1MdAAAJ). However, there are no negative effects of attempting to open multiple other than this throw. * Resolve circular dependency --- .../browser/src/background/main.background.ts | 5 ++-- .../offscreen-document.service.spec.ts | 7 +++++- .../offscreen-document.service.ts | 24 ++++++++++++++----- .../src/popup/services/services.module.ts | 10 ++++---- 4 files changed, 33 insertions(+), 13 deletions(-) diff --git a/apps/browser/src/background/main.background.ts b/apps/browser/src/background/main.background.ts index d5e8fe1da74..41f300270eb 100644 --- a/apps/browser/src/background/main.background.ts +++ b/apps/browser/src/background/main.background.ts @@ -380,7 +380,8 @@ export default class MainBackground { const logoutCallback = async (expired: boolean, userId?: UserId) => await this.logout(expired, userId); - this.logService = new ConsoleLogService(false); + const isDev = process.env.ENV === "development"; + this.logService = new ConsoleLogService(isDev); this.cryptoFunctionService = new WebCryptoFunctionService(self); this.keyGenerationService = new KeyGenerationService(this.cryptoFunctionService); this.storageService = new BrowserLocalStorageService(); @@ -399,7 +400,7 @@ export default class MainBackground { ), ); - this.offscreenDocumentService = new DefaultOffscreenDocumentService(); + this.offscreenDocumentService = new DefaultOffscreenDocumentService(this.logService); this.platformUtilsService = new BackgroundPlatformUtilsService( this.messagingService, diff --git a/apps/browser/src/platform/offscreen-document/offscreen-document.service.spec.ts b/apps/browser/src/platform/offscreen-document/offscreen-document.service.spec.ts index d6be0a924e5..c9bdd823a52 100644 --- a/apps/browser/src/platform/offscreen-document/offscreen-document.service.spec.ts +++ b/apps/browser/src/platform/offscreen-document/offscreen-document.service.spec.ts @@ -1,3 +1,7 @@ +import { mock } from "jest-mock-extended"; + +import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; + import { DefaultOffscreenDocumentService } from "./offscreen-document.service"; class TestCase { @@ -21,6 +25,7 @@ describe.each([ new TestCase("synchronous callback", () => 42), new TestCase("asynchronous callback", () => Promise.resolve(42)), ])("DefaultOffscreenDocumentService %s", (testCase) => { + const logService = mock(); let sut: DefaultOffscreenDocumentService; const reasons = [chrome.offscreen.Reason.TESTING]; const justification = "justification is testing"; @@ -37,7 +42,7 @@ describe.each([ callback = testCase.callback; chrome.offscreen = api; - sut = new DefaultOffscreenDocumentService(); + sut = new DefaultOffscreenDocumentService(logService); }); afterEach(() => { diff --git a/apps/browser/src/platform/offscreen-document/offscreen-document.service.ts b/apps/browser/src/platform/offscreen-document/offscreen-document.service.ts index da0ca382698..a260e3ca6c8 100644 --- a/apps/browser/src/platform/offscreen-document/offscreen-document.service.ts +++ b/apps/browser/src/platform/offscreen-document/offscreen-document.service.ts @@ -1,7 +1,9 @@ +import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; + export class DefaultOffscreenDocumentService implements DefaultOffscreenDocumentService { private workerCount = 0; - constructor() {} + constructor(private logService: LogService) {} async withDocument( reasons: chrome.offscreen.Reason[], @@ -24,11 +26,21 @@ export class DefaultOffscreenDocumentService implements DefaultOffscreenDocument } private async create(reasons: chrome.offscreen.Reason[], justification: string): Promise { - await chrome.offscreen.createDocument({ - url: "offscreen-document/index.html", - reasons, - justification, - }); + try { + await chrome.offscreen.createDocument({ + url: "offscreen-document/index.html", + reasons, + justification, + }); + } catch (e) { + // gobble multiple offscreen document creation errors + // TODO: remove this when the offscreen document service is fixed PM-8014 + if (e.message === "Only a single offscreen document may be created.") { + this.logService.info("Ignoring offscreen document creation error."); + return; + } + throw e; + } } private async close(): Promise { diff --git a/apps/browser/src/popup/services/services.module.ts b/apps/browser/src/popup/services/services.module.ts index 0b9c8f6fe68..32d4adae4a1 100644 --- a/apps/browser/src/popup/services/services.module.ts +++ b/apps/browser/src/popup/services/services.module.ts @@ -195,9 +195,11 @@ const safeProviders: SafeProvider[] = [ }), safeProvider({ provide: LogService, - useFactory: (platformUtilsService: PlatformUtilsService) => - new ConsoleLogService(platformUtilsService.isDev()), - deps: [PlatformUtilsService], + useFactory: () => { + const isDev = process.env.ENV === "development"; + return new ConsoleLogService(isDev); + }, + deps: [], }), safeProvider({ provide: EnvironmentService, @@ -286,7 +288,7 @@ const safeProviders: SafeProvider[] = [ safeProvider({ provide: OffscreenDocumentService, useClass: DefaultOffscreenDocumentService, - deps: [], + deps: [LogService], }), safeProvider({ provide: PlatformUtilsService, From ff19514c27d0d48dff3f5f7b79473ef8807107cd Mon Sep 17 00:00:00 2001 From: Nick Krantz <125900171+nick-livefront@users.noreply.github.com> Date: Wed, 15 May 2024 19:57:59 -0500 Subject: [PATCH 005/123] [PM-7878] PopupSectionHeader component (#9107) * add PopupSectionHeaderComponent * refactor to shorter imports + format * use title as an input * use small icon buttons for section header --- .../popup-section-header.component.html | 11 +++ .../popup-section-header.component.ts | 13 +++ .../popup-section-header.stories.ts | 90 +++++++++++++++++++ apps/browser/src/popup/app.module.ts | 2 + 4 files changed, 116 insertions(+) create mode 100644 apps/browser/src/platform/popup/popup-section-header/popup-section-header.component.html create mode 100644 apps/browser/src/platform/popup/popup-section-header/popup-section-header.component.ts create mode 100644 apps/browser/src/platform/popup/popup-section-header/popup-section-header.stories.ts diff --git a/apps/browser/src/platform/popup/popup-section-header/popup-section-header.component.html b/apps/browser/src/platform/popup/popup-section-header/popup-section-header.component.html new file mode 100644 index 00000000000..6cc7e317e27 --- /dev/null +++ b/apps/browser/src/platform/popup/popup-section-header/popup-section-header.component.html @@ -0,0 +1,11 @@ +
+
+

+ {{ title }} +

+ +
+
+ +
+
diff --git a/apps/browser/src/platform/popup/popup-section-header/popup-section-header.component.ts b/apps/browser/src/platform/popup/popup-section-header/popup-section-header.component.ts new file mode 100644 index 00000000000..b33a2a0f330 --- /dev/null +++ b/apps/browser/src/platform/popup/popup-section-header/popup-section-header.component.ts @@ -0,0 +1,13 @@ +import { Component, Input } from "@angular/core"; + +import { TypographyModule } from "@bitwarden/components"; + +@Component({ + standalone: true, + selector: "popup-section-header", + templateUrl: "./popup-section-header.component.html", + imports: [TypographyModule], +}) +export class PopupSectionHeaderComponent { + @Input() title: string; +} diff --git a/apps/browser/src/platform/popup/popup-section-header/popup-section-header.stories.ts b/apps/browser/src/platform/popup/popup-section-header/popup-section-header.stories.ts new file mode 100644 index 00000000000..450bfb24226 --- /dev/null +++ b/apps/browser/src/platform/popup/popup-section-header/popup-section-header.stories.ts @@ -0,0 +1,90 @@ +import { Meta, StoryObj, moduleMetadata } from "@storybook/angular"; + +import { + CardComponent, + IconButtonModule, + SectionComponent, + TypographyModule, +} from "@bitwarden/components"; + +import { PopupSectionHeaderComponent } from "./popup-section-header.component"; + +export default { + title: "Browser/Popup Section Header", + component: PopupSectionHeaderComponent, + args: { + title: "Title", + }, + decorators: [ + moduleMetadata({ + imports: [SectionComponent, CardComponent, TypographyModule, IconButtonModule], + }), + ], +} as Meta; + +type Story = StoryObj; + +export const OnlyTitle: Story = { + render: (args) => ({ + props: args, + template: ` + + `, + }), + args: { + title: "Only Title", + }, +}; + +export const TrailingText: Story = { + render: (args) => ({ + props: args, + template: ` + + 13 + + `, + }), + args: { + title: "Trailing Text", + }, +}; + +export const TailingIcon: Story = { + render: (args) => ({ + props: args, + template: ` + + + + `, + }), + args: { + title: "Trailing Icon", + }, +}; + +export const WithSections: Story = { + render: () => ({ + template: ` +
+ + + + + +

Card 1 Content

+
+
+ + + + + +

Card 2 Content

+
+
+
+ `, + }), +}; diff --git a/apps/browser/src/popup/app.module.ts b/apps/browser/src/popup/app.module.ts index 05158d3295d..74e24433b2c 100644 --- a/apps/browser/src/popup/app.module.ts +++ b/apps/browser/src/popup/app.module.ts @@ -47,6 +47,7 @@ import { PopupFooterComponent } from "../platform/popup/layout/popup-footer.comp import { PopupHeaderComponent } from "../platform/popup/layout/popup-header.component"; import { PopupPageComponent } from "../platform/popup/layout/popup-page.component"; import { PopupTabNavigationComponent } from "../platform/popup/layout/popup-tab-navigation.component"; +import { PopupSectionHeaderComponent } from "../platform/popup/popup-section-header/popup-section-header.component"; import { FilePopoutCalloutComponent } from "../tools/popup/components/file-popout-callout.component"; import { GeneratorComponent } from "../tools/popup/generator/generator.component"; import { PasswordGeneratorHistoryComponent } from "../tools/popup/generator/password-generator-history.component"; @@ -124,6 +125,7 @@ import "../platform/popup/locales"; PopupFooterComponent, PopupHeaderComponent, UserVerificationDialogComponent, + PopupSectionHeaderComponent, ], declarations: [ ActionButtonsComponent, From 07076ebf9de1bb2cf6fda580ed72e7834fb4641e Mon Sep 17 00:00:00 2001 From: Nick Krantz <125900171+nick-livefront@users.noreply.github.com> Date: Thu, 16 May 2024 08:18:58 -0500 Subject: [PATCH 006/123] [PM-7231] Product Switcher within navigation sidebar (#8810) * refactor: move logic for products into a service - This is in preparation for having having the navigation menu show products based off of the same logic. * add extra small font size to tailwind config * remove absolute positioning from toggle width component - it now sits beneath the product switcher * update product switcher to have UI details that are only shown in the navigation pane * add navigation oriented product switcher * integrate navigation product switcher into secrets manager * integrate navigation product switcher into provider console * integrate navigation product switcher into user layout * integrate navigation product switcher into organizations * add translation for "switch" * hide active styles from navigation product switcher * update storybook for product switcher stories * remove unneeded full width style * use protected readonly variable instead of getter * migrate stories to CSF3 * remove double subscription to `moreProducts$` * only use wrapping div in navigation switcher story - less vertical space is taken up * update to satisfies * refactor `navigationUI` to `otherProductOverrides` * move observables to protected readonly * apply margin-top via class on the host component * remove switch text from the navigation product switcher * Allow for the active navigation switcher to be shown * remove xxs font style * remove unneeded module * remove switch from stories * remove defensive nullish coalescing * remove merge leftovers * Defect PM-7899 - show organizations product at the top of the other products list * Defect PM-7951 use attr.icon to keep the icon as an attribute after prod mode is enabled * Defect PM-7948 update path based on the current org * force active styles for navigation items (#9128) * add horizontal margin to icon --------- Co-authored-by: Thomas Rittson <31796059+eliykat@users.noreply.github.com> --- .../organization-layout.component.html | 8 +- .../layouts/organization-layout.component.ts | 2 + .../navigation-switcher.component.html | 35 +++ .../navigation-switcher.component.spec.ts | 194 ++++++++++++++++ .../navigation-switcher.component.ts | 24 ++ .../navigation-switcher.stories.ts | 171 ++++++++++++++ .../product-switcher-content.component.ts | 138 +---------- .../product-switcher.module.ts | 12 +- .../product-switcher.stories.ts | 67 ++++-- .../shared/product-switcher.service.spec.ts | 216 ++++++++++++++++++ .../shared/product-switcher.service.ts | 189 +++++++++++++++ .../src/app/layouts/toggle-width.component.ts | 1 - .../app/layouts/user-layout.component.html | 4 +- .../src/app/layouts/user-layout.component.ts | 2 + apps/web/src/locales/en/messages.json | 6 + .../providers/providers-layout.component.html | 5 +- .../providers/providers-layout.component.ts | 2 + .../secrets-manager/layout/layout.module.ts | 2 + .../layout/navigation.component.html | 4 +- .../src/navigation/nav-item.component.ts | 7 +- .../src/navigation/nav-item.stories.ts | 11 + 21 files changed, 932 insertions(+), 168 deletions(-) create mode 100644 apps/web/src/app/layouts/product-switcher/navigation-switcher/navigation-switcher.component.html create mode 100644 apps/web/src/app/layouts/product-switcher/navigation-switcher/navigation-switcher.component.spec.ts create mode 100644 apps/web/src/app/layouts/product-switcher/navigation-switcher/navigation-switcher.component.ts create mode 100644 apps/web/src/app/layouts/product-switcher/navigation-switcher/navigation-switcher.stories.ts create mode 100644 apps/web/src/app/layouts/product-switcher/shared/product-switcher.service.spec.ts create mode 100644 apps/web/src/app/layouts/product-switcher/shared/product-switcher.service.ts diff --git a/apps/web/src/app/admin-console/organizations/layouts/organization-layout.component.html b/apps/web/src/app/admin-console/organizations/layouts/organization-layout.component.html index 2b3be149749..d1a48a78e11 100644 --- a/apps/web/src/app/admin-console/organizations/layouts/organization-layout.component.html +++ b/apps/web/src/app/admin-console/organizations/layouts/organization-layout.component.html @@ -1,5 +1,9 @@ -