From c787ecd22c250371f357433eab53f4a953f2f3c2 Mon Sep 17 00:00:00 2001 From: Andreas Coroiu Date: Fri, 18 Oct 2024 16:15:10 +0200 Subject: [PATCH 1/7] [PM-11764] Implement account switching and sdk initialization (#11472) * feat: update sdk service abstraction with documentation and new `userClient$` function * feat: add uninitialized user client with cache * feat: initialize user crypto * feat: initialize org keys * fix: org crypto not initializing properly * feat: avoid creating clients unnecessarily * chore: remove dev print/subscription * fix: clean up cache * chore: update sdk version * feat: implement clean-up logic (#11504) * chore: bump sdk version to fix build issues * chore: bump sdk version to fix build issues * fix: missing constructor parameters * refactor: simplify free() and delete() calls * refactor: use a named function for client creation * fix: client never freeing after refactor * fix: broken impl and race condition in tests --- .../browser/src/background/main.background.ts | 3 + .../service-container/service-container.ts | 3 + .../src/services/jslib-services.module.ts | 3 + .../auth/abstractions/kdf-config.service.ts | 7 +- .../src/auth/services/kdf-config.service.ts | 6 +- .../platform/abstractions/crypto.service.ts | 26 ++- .../platform/abstractions/sdk/sdk.service.ts | 20 ++- .../src/platform/services/crypto.service.ts | 10 ++ .../services/sdk/default-sdk.service.spec.ts | 132 +++++++++++++++ .../services/sdk/default-sdk.service.ts | 155 ++++++++++++++++-- package-lock.json | 9 +- package.json | 2 +- 12 files changed, 355 insertions(+), 21 deletions(-) create mode 100644 libs/common/src/platform/services/sdk/default-sdk.service.spec.ts diff --git a/apps/browser/src/background/main.background.ts b/apps/browser/src/background/main.background.ts index 819bb2a59f9..a3dd1c473ae 100644 --- a/apps/browser/src/background/main.background.ts +++ b/apps/browser/src/background/main.background.ts @@ -731,6 +731,9 @@ export default class MainBackground { sdkClientFactory, this.environmentService, this.platformUtilsService, + this.accountService, + this.kdfConfigService, + this.cryptoService, this.apiService, ); diff --git a/apps/cli/src/service-container/service-container.ts b/apps/cli/src/service-container/service-container.ts index 8f8f1fa4563..f3d71462f6b 100644 --- a/apps/cli/src/service-container/service-container.ts +++ b/apps/cli/src/service-container/service-container.ts @@ -535,6 +535,9 @@ export class ServiceContainer { sdkClientFactory, this.environmentService, this.platformUtilsService, + this.accountService, + this.kdfConfigService, + this.cryptoService, this.apiService, customUserAgent, ); diff --git a/libs/angular/src/services/jslib-services.module.ts b/libs/angular/src/services/jslib-services.module.ts index cc7af0c0b05..6af0fe2f660 100644 --- a/libs/angular/src/services/jslib-services.module.ts +++ b/libs/angular/src/services/jslib-services.module.ts @@ -1334,6 +1334,9 @@ const safeProviders: SafeProvider[] = [ SdkClientFactory, EnvironmentService, PlatformUtilsServiceAbstraction, + AccountServiceAbstraction, + KdfConfigServiceAbstraction, + CryptoServiceAbstraction, ApiServiceAbstraction, ], }), diff --git a/libs/common/src/auth/abstractions/kdf-config.service.ts b/libs/common/src/auth/abstractions/kdf-config.service.ts index 6b41979e1b9..f4ffe31baa4 100644 --- a/libs/common/src/auth/abstractions/kdf-config.service.ts +++ b/libs/common/src/auth/abstractions/kdf-config.service.ts @@ -1,7 +1,10 @@ +import { Observable } from "rxjs"; + import { UserId } from "../../types/guid"; import { KdfConfig } from "../models/domain/kdf-config"; export abstract class KdfConfigService { - setKdfConfig: (userId: UserId, KdfConfig: KdfConfig) => Promise; - getKdfConfig: () => Promise; + abstract setKdfConfig(userId: UserId, KdfConfig: KdfConfig): Promise; + abstract getKdfConfig(): Promise; + abstract getKdfConfig$(userId: UserId): Observable; } diff --git a/libs/common/src/auth/services/kdf-config.service.ts b/libs/common/src/auth/services/kdf-config.service.ts index cfd2a3e1de0..604a186d765 100644 --- a/libs/common/src/auth/services/kdf-config.service.ts +++ b/libs/common/src/auth/services/kdf-config.service.ts @@ -1,4 +1,4 @@ -import { firstValueFrom } from "rxjs"; +import { firstValueFrom, Observable } from "rxjs"; import { KdfType } from "../../platform/enums/kdf-type.enum"; import { KDF_CONFIG_DISK, StateProvider, UserKeyDefinition } from "../../platform/state"; @@ -38,4 +38,8 @@ export class KdfConfigService implements KdfConfigServiceAbstraction { } return state; } + + getKdfConfig$(userId: UserId): Observable { + return this.stateProvider.getUser(userId, KDF_CONFIG).state$; + } } diff --git a/libs/common/src/platform/abstractions/crypto.service.ts b/libs/common/src/platform/abstractions/crypto.service.ts index 020cfb81754..0a554f6249b 100644 --- a/libs/common/src/platform/abstractions/crypto.service.ts +++ b/libs/common/src/platform/abstractions/crypto.service.ts @@ -1,5 +1,6 @@ import { Observable } from "rxjs"; +import { EncryptedOrganizationKeyData } from "../../admin-console/models/data/encrypted-organization-key.data"; import { ProfileOrganizationResponse } from "../../admin-console/models/response/profile-organization.response"; import { ProfileProviderOrganizationResponse } from "../../admin-console/models/response/profile-provider-organization.response"; import { ProfileProviderResponse } from "../../admin-console/models/response/profile-provider.response"; @@ -15,7 +16,7 @@ import { UserPublicKey, } from "../../types/key"; import { KeySuffixOptions, HashPurpose } from "../enums"; -import { EncString } from "../models/domain/enc-string"; +import { EncryptedString, EncString } from "../models/domain/enc-string"; import { SymmetricCryptoKey } from "../models/domain/symmetric-crypto-key"; export class UserPrivateKeyDecryptionFailedError extends Error { @@ -288,6 +289,17 @@ export abstract class CryptoService { */ abstract userPrivateKey$(userId: UserId): Observable; + /** + * Gets an observable stream of the given users encrypted private key, will emit null if the user + * doesn't have an encrypted private key at all. + * + * @param userId The user id of the user to get the data for. + * + * @deprecated Temporary function to allow the SDK to be initialized after the login process, it + * will be removed when auth has been migrated to the SDK. + */ + abstract userEncryptedPrivateKey$(userId: UserId): Observable; + /** * Gets an observable stream of the given users decrypted private key with legacy support, * will emit null if the user doesn't have a UserKey to decrypt the encrypted private key @@ -381,6 +393,18 @@ export abstract class CryptoService { */ abstract orgKeys$(userId: UserId): Observable | null>; + /** + * Gets an observable stream of the given users encrypted organisation keys. + * + * @param userId The user id of the user to get the data for. + * + * @deprecated Temporary function to allow the SDK to be initialized after the login process, it + * will be removed when auth has been migrated to the SDK. + */ + abstract encryptedOrgKeys$( + userId: UserId, + ): Observable>; + /** * Gets an observable stream of the users public key. If the user is does not have * a {@link UserKey} or {@link UserPrivateKey} that is decryptable, this will emit null. diff --git a/libs/common/src/platform/abstractions/sdk/sdk.service.ts b/libs/common/src/platform/abstractions/sdk/sdk.service.ts index 360f2e91a76..5e4e4cb4cbe 100644 --- a/libs/common/src/platform/abstractions/sdk/sdk.service.ts +++ b/libs/common/src/platform/abstractions/sdk/sdk.service.ts @@ -2,9 +2,27 @@ import { Observable } from "rxjs"; import { BitwardenClient } from "@bitwarden/sdk-internal"; +import { UserId } from "../../../types/guid"; + export abstract class SdkService { - client$: Observable; + /** + * Check if the SDK is supported in the current environment. + */ supported$: Observable; + /** + * Retrieve a client initialized without a user. + * This client can only be used for operations that don't require a user context. + */ + client$: Observable; + + /** + * Retrieve a client initialized for a specific user. + * This client can be used for operations that require a user context, such as retrieving ciphers + * and operations involving crypto. It can also be used for operations that don't require a user context. + * @param userId + */ + abstract userClient$(userId: UserId): Observable; + abstract failedToInitialize(): Promise; } diff --git a/libs/common/src/platform/services/crypto.service.ts b/libs/common/src/platform/services/crypto.service.ts index 6b2afdb9806..a6db9a2c1bf 100644 --- a/libs/common/src/platform/services/crypto.service.ts +++ b/libs/common/src/platform/services/crypto.service.ts @@ -841,6 +841,10 @@ export class CryptoService implements CryptoServiceAbstraction { return this.userPrivateKeyHelper$(userId, false).pipe(map((keys) => keys?.userPrivateKey)); } + userEncryptedPrivateKey$(userId: UserId): Observable { + return this.stateProvider.getUser(userId, USER_ENCRYPTED_PRIVATE_KEY).state$; + } + userPrivateKeyWithLegacySupport$(userId: UserId): Observable { return this.userPrivateKeyHelper$(userId, true).pipe(map((keys) => keys?.userPrivateKey)); } @@ -929,6 +933,12 @@ export class CryptoService implements CryptoServiceAbstraction { return this.cipherDecryptionKeys$(userId, true).pipe(map((keys) => keys?.orgKeys)); } + encryptedOrgKeys$( + userId: UserId, + ): Observable> { + return this.stateProvider.getUser(userId, USER_ENCRYPTED_ORGANIZATION_KEYS).state$; + } + cipherDecryptionKeys$( userId: UserId, legacySupport: boolean = false, diff --git a/libs/common/src/platform/services/sdk/default-sdk.service.spec.ts b/libs/common/src/platform/services/sdk/default-sdk.service.spec.ts new file mode 100644 index 00000000000..dad99401f75 --- /dev/null +++ b/libs/common/src/platform/services/sdk/default-sdk.service.spec.ts @@ -0,0 +1,132 @@ +import { mock, MockProxy } from "jest-mock-extended"; +import { BehaviorSubject, firstValueFrom, of } from "rxjs"; + +import { BitwardenClient } from "@bitwarden/sdk-internal"; + +import { ApiService } from "../../../abstractions/api.service"; +import { AccountInfo, AccountService } from "../../../auth/abstractions/account.service"; +import { KdfConfigService } from "../../../auth/abstractions/kdf-config.service"; +import { PBKDF2KdfConfig } from "../../../auth/models/domain/kdf-config"; +import { UserId } from "../../../types/guid"; +import { UserKey } from "../../../types/key"; +import { CryptoService } from "../../abstractions/crypto.service"; +import { Environment, EnvironmentService } from "../../abstractions/environment.service"; +import { PlatformUtilsService } from "../../abstractions/platform-utils.service"; +import { SdkClientFactory } from "../../abstractions/sdk/sdk-client-factory"; +import { EncryptedString } from "../../models/domain/enc-string"; +import { SymmetricCryptoKey } from "../../models/domain/symmetric-crypto-key"; + +import { DefaultSdkService } from "./default-sdk.service"; + +describe("DefaultSdkService", () => { + describe("userClient$", () => { + let sdkClientFactory!: MockProxy; + let environmentService!: MockProxy; + let platformUtilsService!: MockProxy; + let accountService!: MockProxy; + let kdfConfigService!: MockProxy; + let cryptoService!: MockProxy; + let apiService!: MockProxy; + let service!: DefaultSdkService; + + let mockClient!: MockProxy; + + beforeEach(() => { + sdkClientFactory = mock(); + environmentService = mock(); + platformUtilsService = mock(); + accountService = mock(); + kdfConfigService = mock(); + cryptoService = mock(); + apiService = mock(); + + // Can't use `of(mock())` for some reason + environmentService.environment$ = new BehaviorSubject(mock()); + + service = new DefaultSdkService( + sdkClientFactory, + environmentService, + platformUtilsService, + accountService, + kdfConfigService, + cryptoService, + apiService, + ); + + mockClient = mock(); + mockClient.crypto.mockReturnValue(mock()); + sdkClientFactory.createSdkClient.mockResolvedValue(mockClient); + }); + + describe("given the user is logged in", () => { + const userId = "user-id" as UserId; + + beforeEach(() => { + accountService.accounts$ = of({ + [userId]: { email: "email", emailVerified: true, name: "name" } as AccountInfo, + }); + kdfConfigService.getKdfConfig$ + .calledWith(userId) + .mockReturnValue(of(new PBKDF2KdfConfig())); + cryptoService.userKey$ + .calledWith(userId) + .mockReturnValue(of(new SymmetricCryptoKey(new Uint8Array(64)) as UserKey)); + cryptoService.userEncryptedPrivateKey$ + .calledWith(userId) + .mockReturnValue(of("private-key" as EncryptedString)); + cryptoService.encryptedOrgKeys$.calledWith(userId).mockReturnValue(of({})); + }); + + it("creates an SDK client when called the first time", async () => { + const result = await firstValueFrom(service.userClient$(userId)); + + expect(result).toBe(mockClient); + expect(sdkClientFactory.createSdkClient).toHaveBeenCalled(); + }); + + it("does not create an SDK client when called the second time with same userId", async () => { + const subject_1 = new BehaviorSubject(undefined); + const subject_2 = new BehaviorSubject(undefined); + + // Use subjects to ensure the subscription is kept alive + service.userClient$(userId).subscribe(subject_1); + service.userClient$(userId).subscribe(subject_2); + + // Wait for the next tick to ensure all async operations are done + await new Promise(process.nextTick); + + expect(subject_1.value).toBe(mockClient); + expect(subject_2.value).toBe(mockClient); + expect(sdkClientFactory.createSdkClient).toHaveBeenCalledTimes(1); + }); + + it("destroys the SDK client when all subscriptions are closed", async () => { + const subject_1 = new BehaviorSubject(undefined); + const subject_2 = new BehaviorSubject(undefined); + const subscription_1 = service.userClient$(userId).subscribe(subject_1); + const subscription_2 = service.userClient$(userId).subscribe(subject_2); + await new Promise(process.nextTick); + + subscription_1.unsubscribe(); + subscription_2.unsubscribe(); + + expect(mockClient.free).toHaveBeenCalledTimes(1); + }); + + it("destroys the SDK client when the userKey is unset (i.e. lock or logout)", async () => { + const userKey$ = new BehaviorSubject(new SymmetricCryptoKey(new Uint8Array(64)) as UserKey); + cryptoService.userKey$.calledWith(userId).mockReturnValue(userKey$); + + const subject = new BehaviorSubject(undefined); + service.userClient$(userId).subscribe(subject); + await new Promise(process.nextTick); + + userKey$.next(undefined); + await new Promise(process.nextTick); + + expect(mockClient.free).toHaveBeenCalledTimes(1); + expect(subject.value).toBe(undefined); + }); + }); + }); +}); diff --git a/libs/common/src/platform/services/sdk/default-sdk.service.ts b/libs/common/src/platform/services/sdk/default-sdk.service.ts index d4a9cfeb7ed..1b7a9a939a4 100644 --- a/libs/common/src/platform/services/sdk/default-sdk.service.ts +++ b/libs/common/src/platform/services/sdk/default-sdk.service.ts @@ -1,24 +1,45 @@ -import { concatMap, firstValueFrom, shareReplay } from "rxjs"; +import { + combineLatest, + concatMap, + firstValueFrom, + Observable, + shareReplay, + map, + distinctUntilChanged, + tap, + switchMap, +} from "rxjs"; -import { LogLevel, DeviceType as SdkDeviceType } from "@bitwarden/sdk-internal"; +import { + BitwardenClient, + ClientSettings, + LogLevel, + DeviceType as SdkDeviceType, +} from "@bitwarden/sdk-internal"; import { ApiService } from "../../../abstractions/api.service"; +import { EncryptedOrganizationKeyData } from "../../../admin-console/models/data/encrypted-organization-key.data"; +import { AccountInfo, AccountService } from "../../../auth/abstractions/account.service"; +import { KdfConfigService } from "../../../auth/abstractions/kdf-config.service"; +import { KdfConfig } from "../../../auth/models/domain/kdf-config"; import { DeviceType } from "../../../enums/device-type.enum"; -import { EnvironmentService } from "../../abstractions/environment.service"; +import { OrganizationId, UserId } from "../../../types/guid"; +import { UserKey } from "../../../types/key"; +import { CryptoService } from "../../abstractions/crypto.service"; +import { Environment, EnvironmentService } from "../../abstractions/environment.service"; import { PlatformUtilsService } from "../../abstractions/platform-utils.service"; import { SdkClientFactory } from "../../abstractions/sdk/sdk-client-factory"; import { SdkService } from "../../abstractions/sdk/sdk.service"; +import { KdfType } from "../../enums"; +import { compareValues } from "../../misc/compare-values"; +import { EncryptedString } from "../../models/domain/enc-string"; export class DefaultSdkService implements SdkService { + private sdkClientCache = new Map>(); + client$ = this.environmentService.environment$.pipe( concatMap(async (env) => { - const settings = { - apiUrl: env.getApiUrl(), - identityUrl: env.getIdentityUrl(), - deviceType: this.toDevice(this.platformUtilsService.getDevice()), - userAgent: this.userAgent ?? navigator.userAgent, - }; - + const settings = this.toSettings(env); return await this.sdkClientFactory.createSdkClient(settings, LogLevel.Info); }), shareReplay({ refCount: true, bufferSize: 1 }), @@ -34,10 +55,81 @@ export class DefaultSdkService implements SdkService { private sdkClientFactory: SdkClientFactory, private environmentService: EnvironmentService, private platformUtilsService: PlatformUtilsService, + private accountService: AccountService, + private kdfConfigService: KdfConfigService, + private cryptoService: CryptoService, private apiService: ApiService, // Yes we shouldn't import ApiService, but it's temporary private userAgent: string = null, ) {} + userClient$(userId: UserId): Observable { + // TODO: Figure out what happens when the user logs out + if (this.sdkClientCache.has(userId)) { + return this.sdkClientCache.get(userId); + } + + const account$ = this.accountService.accounts$.pipe( + map((accounts) => accounts[userId]), + distinctUntilChanged(), + ); + const kdfParams$ = this.kdfConfigService.getKdfConfig$(userId).pipe(distinctUntilChanged()); + const privateKey$ = this.cryptoService + .userEncryptedPrivateKey$(userId) + .pipe(distinctUntilChanged()); + const userKey$ = this.cryptoService.userKey$(userId).pipe(distinctUntilChanged()); + const orgKeys$ = this.cryptoService.encryptedOrgKeys$(userId).pipe( + distinctUntilChanged(compareValues), // The upstream observable emits different objects with the same values + ); + + const client$ = combineLatest([ + this.environmentService.environment$, + account$, + kdfParams$, + privateKey$, + userKey$, + orgKeys$, + ]).pipe( + // switchMap is required to allow the clean-up logic to be executed when `combineLatest` emits a new value. + switchMap(([env, account, kdfParams, privateKey, userKey, orgKeys]) => { + // Create our own observable to be able to implement clean-up logic + return new Observable((subscriber) => { + let client: BitwardenClient; + + const createAndInitializeClient = async () => { + if (privateKey == null || userKey == null || orgKeys == null) { + return undefined; + } + + const settings = this.toSettings(env); + client = await this.sdkClientFactory.createSdkClient(settings, LogLevel.Info); + + await this.initializeClient(client, account, kdfParams, privateKey, userKey, orgKeys); + + return client; + }; + + createAndInitializeClient() + .then((c) => { + client = c; + subscriber.next(c); + }) + .catch((e) => { + subscriber.error(e); + }); + + return () => client?.free(); + }); + }), + tap({ + finalize: () => this.sdkClientCache.delete(userId), + }), + shareReplay({ refCount: true, bufferSize: 1 }), + ); + + this.sdkClientCache.set(userId, client$); + return client$; + } + async failedToInitialize(): Promise { // Only log on cloud instances if ( @@ -52,6 +144,49 @@ export class DefaultSdkService implements SdkService { }); } + private async initializeClient( + client: BitwardenClient, + account: AccountInfo, + kdfParams: KdfConfig, + privateKey: EncryptedString, + userKey: UserKey, + orgKeys: Record, + ) { + await client.crypto().initialize_user_crypto({ + email: account.email, + method: { decryptedKey: { decrypted_user_key: userKey.keyB64 } }, + kdfParams: + kdfParams.kdfType === KdfType.PBKDF2_SHA256 + ? { + pBKDF2: { iterations: kdfParams.iterations }, + } + : { + argon2id: { + iterations: kdfParams.iterations, + memory: kdfParams.memory, + parallelism: kdfParams.parallelism, + }, + }, + privateKey, + }); + await client.crypto().initialize_org_crypto({ + organizationKeys: new Map( + Object.entries(orgKeys) + .filter(([_, v]) => v.type === "organization") + .map(([k, v]) => [k, v.key]), + ), + }); + } + + private toSettings(env: Environment): ClientSettings { + return { + apiUrl: env.getApiUrl(), + identityUrl: env.getIdentityUrl(), + deviceType: this.toDevice(this.platformUtilsService.getDevice()), + userAgent: this.userAgent ?? navigator.userAgent, + }; + } + private toDevice(device: DeviceType): SdkDeviceType { switch (device) { case DeviceType.Android: diff --git a/package-lock.json b/package-lock.json index 2dcfda73888..c679767699a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -24,7 +24,7 @@ "@angular/platform-browser": "16.2.12", "@angular/platform-browser-dynamic": "16.2.12", "@angular/router": "16.2.12", - "@bitwarden/sdk-internal": "0.1.3", + "@bitwarden/sdk-internal": "0.1.6", "@electron/fuses": "1.8.0", "@koa/multer": "3.0.2", "@koa/router": "13.1.0", @@ -4696,10 +4696,9 @@ "link": true }, "node_modules/@bitwarden/sdk-internal": { - "version": "0.1.3", - "resolved": "https://registry.npmjs.org/@bitwarden/sdk-internal/-/sdk-internal-0.1.3.tgz", - "integrity": "sha512-zk9DyYMjylVLdljeLn3OLBcD939Hg/qMNJ2FxbyjiSKtcOcgglXgYmbcS01NRFFfM9REbn+j+2fWbQo6N+8SHw==", - "license": "SEE LICENSE IN LICENSE" + "version": "0.1.6", + "resolved": "https://registry.npmjs.org/@bitwarden/sdk-internal/-/sdk-internal-0.1.6.tgz", + "integrity": "sha512-YUOOcXnK004mAwE+vfy7AgeLYCtTyafYaXEWED3PNRaSun/a5elrAD//h2yuF9u8Dn5jg1VDkssMPpuG9+2VxA==" }, "node_modules/@bitwarden/vault": { "resolved": "libs/vault", diff --git a/package.json b/package.json index 402b7c482d9..38440adf92f 100644 --- a/package.json +++ b/package.json @@ -158,7 +158,7 @@ "@angular/platform-browser": "16.2.12", "@angular/platform-browser-dynamic": "16.2.12", "@angular/router": "16.2.12", - "@bitwarden/sdk-internal": "0.1.3", + "@bitwarden/sdk-internal": "0.1.6", "@electron/fuses": "1.8.0", "@koa/multer": "3.0.2", "@koa/router": "13.1.0", From ad40db9ecf45213ea0fb6cd37c792a092dcf8d4d Mon Sep 17 00:00:00 2001 From: Jared Snider <116684653+JaredSnider-Bitwarden@users.noreply.github.com> Date: Fri, 18 Oct 2024 10:17:23 -0400 Subject: [PATCH 2/7] PM-13368 - Registration with Email Verification - Registration Finish - Add 2FA support (#11614) --- .../registration-finish/registration-finish.component.ts | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/libs/auth/src/angular/registration/registration-finish/registration-finish.component.ts b/libs/auth/src/angular/registration/registration-finish/registration-finish.component.ts index be9d8abe5b0..3a6d26ef939 100644 --- a/libs/auth/src/angular/registration/registration-finish/registration-finish.component.ts +++ b/libs/auth/src/angular/registration/registration-finish/registration-finish.component.ts @@ -163,7 +163,12 @@ export class RegistrationFinishComponent implements OnInit, OnDestroy { null, ); - await this.loginStrategyService.logIn(credentials); + const authenticationResult = await this.loginStrategyService.logIn(credentials); + + if (authenticationResult?.requiresTwoFactor) { + await this.router.navigate(["/2fa"]); + return; + } this.toastService.showToast({ variant: "success", From 2605c03c816dd5f53a4e3fb10e8bf3a8091ea796 Mon Sep 17 00:00:00 2001 From: Jared Snider <116684653+JaredSnider-Bitwarden@users.noreply.github.com> Date: Fri, 18 Oct 2024 12:14:29 -0400 Subject: [PATCH 3/7] Update Icon Docs to include information on viewbox scaling (#11613) --- libs/components/src/icon/icon.mdx | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/libs/components/src/icon/icon.mdx b/libs/components/src/icon/icon.mdx index d8b881b7e86..fc1c4cd3d57 100644 --- a/libs/components/src/icon/icon.mdx +++ b/libs/components/src/icon/icon.mdx @@ -67,7 +67,14 @@ import * as stories from "./icon.stories"; - Example: `--color-art-primary` corresponds to `tw-stroke-art-primary` or `tw-fill-art-primary`. -6. **Import your SVG const** anywhere you want to use the SVG. +6. **Remove any hardcoded width or height attributes** if your SVG has a configured + [viewBox](https://developer.mozilla.org/en-US/docs/Web/SVG/Attribute/viewBox) attribute in order + to allow the SVG to scale to fit its container. + + - **Note:** Scaling is required for any SVG used as an + [AnonLayout](?path=/docs/auth-anon-layout--docs) `pageIcon`. + +7. **Import your SVG const** anywhere you want to use the SVG. - **Angular Component Example:** @@ -95,5 +102,5 @@ import * as stories from "./icon.stories"; ``` -7. **Ensure your SVG renders properly** according to Figma in both light and dark modes on a client +8. **Ensure your SVG renders properly** according to Figma in both light and dark modes on a client which supports multiple style modes. From 4768f7c0fa72330011f6f9a7950f881fc5ae1717 Mon Sep 17 00:00:00 2001 From: Mark Youssef <141061617+mark-youssef-bitwarden@users.noreply.github.com> Date: Fri, 18 Oct 2024 10:25:33 -0700 Subject: [PATCH 4/7] [Marketing] Update testimonial in free registration page (#11603) * Update testimonial in free registration page * Fix wired logo alt text Co-authored-by: rr-bw <102181210+rr-bw@users.noreply.github.com> --------- Co-authored-by: rr-bw <102181210+rr-bw@users.noreply.github.com> --- .../content/default-content.component.html | 5 ++-- .../logo-company-testimonial.component.html | 28 +++++++++++++++++++ .../logo-company-testimonial.component.ts | 7 +++++ .../trial-initiation.module.ts | 2 ++ .../register-layout/new-york-times-logo.svg | 4 +++ .../src/images/register-layout/pcmag-logo.svg | 16 +++++++++++ 6 files changed, 59 insertions(+), 3 deletions(-) create mode 100644 apps/web/src/app/auth/trial-initiation/content/logo-company-testimonial.component.html create mode 100644 apps/web/src/app/auth/trial-initiation/content/logo-company-testimonial.component.ts create mode 100644 apps/web/src/images/register-layout/new-york-times-logo.svg create mode 100644 apps/web/src/images/register-layout/pcmag-logo.svg diff --git a/apps/web/src/app/auth/trial-initiation/content/default-content.component.html b/apps/web/src/app/auth/trial-initiation/content/default-content.component.html index 46e1fae80df..e1839517ff6 100644 --- a/apps/web/src/app/auth/trial-initiation/content/default-content.component.html +++ b/apps/web/src/app/auth/trial-initiation/content/default-content.component.html @@ -11,7 +11,6 @@
  • Access anywhere on any device
  • Create your account to get started
  • -
    - - +
    +
    diff --git a/apps/web/src/app/auth/trial-initiation/content/logo-company-testimonial.component.html b/apps/web/src/app/auth/trial-initiation/content/logo-company-testimonial.component.html new file mode 100644 index 00000000000..0b81e0bd216 --- /dev/null +++ b/apps/web/src/app/auth/trial-initiation/content/logo-company-testimonial.component.html @@ -0,0 +1,28 @@ +
    +

    + Recommended by industry experts +

    +
    +
    + CNET Logo + WIRED Logo +
    +
    + New York Times Logo + PC Mag Logo +
    +
    +
    + “Bitwarden is currently CNET's top pick for the best password manager, thanks in part to + its commitment to transparency and its unbeatable free tier.” +
    +

    Best Password Manager in 2024

    +
    diff --git a/apps/web/src/app/auth/trial-initiation/content/logo-company-testimonial.component.ts b/apps/web/src/app/auth/trial-initiation/content/logo-company-testimonial.component.ts new file mode 100644 index 00000000000..9d9c4471820 --- /dev/null +++ b/apps/web/src/app/auth/trial-initiation/content/logo-company-testimonial.component.ts @@ -0,0 +1,7 @@ +import { Component } from "@angular/core"; + +@Component({ + selector: "app-logo-company-testimonial", + templateUrl: "logo-company-testimonial.component.html", +}) +export class LogoCompanyTestimonialComponent {} diff --git a/apps/web/src/app/auth/trial-initiation/trial-initiation.module.ts b/apps/web/src/app/auth/trial-initiation/trial-initiation.module.ts index 9a7ed7e429d..464c00c4a3a 100644 --- a/apps/web/src/app/auth/trial-initiation/trial-initiation.module.ts +++ b/apps/web/src/app/auth/trial-initiation/trial-initiation.module.ts @@ -29,6 +29,7 @@ import { Enterprise2ContentComponent } from "./content/enterprise2-content.compo import { LogoBadgesComponent } from "./content/logo-badges.component"; import { LogoCnet5StarsComponent } from "./content/logo-cnet-5-stars.component"; import { LogoCnetComponent } from "./content/logo-cnet.component"; +import { LogoCompanyTestimonialComponent } from "./content/logo-company-testimonial.component"; import { LogoForbesComponent } from "./content/logo-forbes.component"; import { LogoUSNewsComponent } from "./content/logo-us-news.component"; import { ReviewBlurbComponent } from "./content/review-blurb.component"; @@ -76,6 +77,7 @@ import { VerticalStepperModule } from "./vertical-stepper/vertical-stepper.modul AbmTeamsContentComponent, LogoBadgesComponent, LogoCnet5StarsComponent, + LogoCompanyTestimonialComponent, LogoCnetComponent, LogoForbesComponent, LogoUSNewsComponent, diff --git a/apps/web/src/images/register-layout/new-york-times-logo.svg b/apps/web/src/images/register-layout/new-york-times-logo.svg new file mode 100644 index 00000000000..3434e1d4164 --- /dev/null +++ b/apps/web/src/images/register-layout/new-york-times-logo.svg @@ -0,0 +1,4 @@ + + + \ No newline at end of file diff --git a/apps/web/src/images/register-layout/pcmag-logo.svg b/apps/web/src/images/register-layout/pcmag-logo.svg new file mode 100644 index 00000000000..af474fdf847 --- /dev/null +++ b/apps/web/src/images/register-layout/pcmag-logo.svg @@ -0,0 +1,16 @@ + + + + + + + + + + \ No newline at end of file From 0e23f5e0cde37fa9c27260f8ee4d7ceed31fc809 Mon Sep 17 00:00:00 2001 From: Jordan Aasen <166539328+jaasen-livefront@users.noreply.github.com> Date: Fri, 18 Oct 2024 11:06:23 -0700 Subject: [PATCH 5/7] fix expiration date (#11625) --- .../popup/send-v2/send-created/send-created.component.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/apps/browser/src/tools/popup/send-v2/send-created/send-created.component.ts b/apps/browser/src/tools/popup/send-v2/send-created/send-created.component.ts index ae66d14d3f0..88475d7dad9 100644 --- a/apps/browser/src/tools/popup/send-v2/send-created/send-created.component.ts +++ b/apps/browser/src/tools/popup/send-v2/send-created/send-created.component.ts @@ -65,11 +65,11 @@ export class SendCreatedComponent { if (this.hoursAvailable < 24) { return this.hoursAvailable === 1 ? this.i18nService.t("sendExpiresInHoursSingle") - : this.i18nService.t("sendExpiresInHours", this.hoursAvailable); + : this.i18nService.t("sendExpiresInHours", String(this.hoursAvailable)); } return this.daysAvailable === 1 ? this.i18nService.t("sendExpiresInDaysSingle") - : this.i18nService.t("sendExpiresInDays", this.daysAvailable); + : this.i18nService.t("sendExpiresInDays", String(this.daysAvailable)); } getHoursAvailable(send: SendView): number { From 81d1274111636040b1f3e40d0bd1547b58a12550 Mon Sep 17 00:00:00 2001 From: Zilong Xue <114327061+ZilongXue@users.noreply.github.com> Date: Fri, 18 Oct 2024 13:37:32 -0500 Subject: [PATCH 6/7] Fix: Add TOTP import support to KeePassX CSV importer (#11574) KeePassX CSV importer was missing TOTP field support. Added logic to parse TOTP fields from the CSV and include them in the vault entries. Added unit tests to verify TOTP import functionality. --- .../spec/keepassx-csv-importer.spec.ts | 42 +++++++++++++++++++ .../test-data/keepassx-csv/testdata.csv.ts | 3 ++ .../src/importers/keepassx-csv-importer.ts | 2 + 3 files changed, 47 insertions(+) create mode 100644 libs/importer/spec/keepassx-csv-importer.spec.ts create mode 100644 libs/importer/spec/test-data/keepassx-csv/testdata.csv.ts diff --git a/libs/importer/spec/keepassx-csv-importer.spec.ts b/libs/importer/spec/keepassx-csv-importer.spec.ts new file mode 100644 index 00000000000..0b3d729d9de --- /dev/null +++ b/libs/importer/spec/keepassx-csv-importer.spec.ts @@ -0,0 +1,42 @@ +import { KeePassXCsvImporter } from "../src/importers"; + +import { keepassxTestData } from "./test-data/keepassx-csv/testdata.csv"; + +describe("KeePassX CSV Importer", () => { + let importer: KeePassXCsvImporter; + + beforeEach(() => { + importer = new KeePassXCsvImporter(); + }); + + describe("given login data", () => { + it("should parse login data when provided valid CSV", async () => { + const result = await importer.parse(keepassxTestData); + expect(result != null).toBe(true); + + const cipher = result.ciphers.shift(); + expect(cipher.name).toEqual("Example Entry"); + expect(cipher.login.username).toEqual("testuser"); + expect(cipher.login.password).toEqual("password123"); + expect(cipher.login.uris.length).toEqual(1); + const uriView = cipher.login.uris.shift(); + expect(uriView.uri).toEqual("https://example.com"); + expect(cipher.notes).toEqual("Some notes"); + }); + + it("should import TOTP when present in the CSV", async () => { + const result = await importer.parse(keepassxTestData); + expect(result != null).toBe(true); + + const cipher = result.ciphers.pop(); + expect(cipher.name).toEqual("Another Entry"); + expect(cipher.login.username).toEqual("anotheruser"); + expect(cipher.login.password).toEqual("anotherpassword"); + expect(cipher.login.uris.length).toEqual(1); + const uriView = cipher.login.uris.shift(); + expect(uriView.uri).toEqual("https://another.com"); + expect(cipher.notes).toEqual("Another set of notes"); + expect(cipher.login.totp).toEqual("otpauth://totp/Another?secret=ABCD1234EFGH5678"); + }); + }); +}); diff --git a/libs/importer/spec/test-data/keepassx-csv/testdata.csv.ts b/libs/importer/spec/test-data/keepassx-csv/testdata.csv.ts new file mode 100644 index 00000000000..99eb99b993a --- /dev/null +++ b/libs/importer/spec/test-data/keepassx-csv/testdata.csv.ts @@ -0,0 +1,3 @@ +export const keepassxTestData = `Title,Username,Password,URL,Notes,TOTP +Example Entry,testuser,password123,https://example.com,Some notes, +Another Entry,anotheruser,anotherpassword,https://another.com,Another set of notes,otpauth://totp/Another?secret=ABCD1234EFGH5678`; diff --git a/libs/importer/src/importers/keepassx-csv-importer.ts b/libs/importer/src/importers/keepassx-csv-importer.ts index 4047a49d572..03aa18cecba 100644 --- a/libs/importer/src/importers/keepassx-csv-importer.ts +++ b/libs/importer/src/importers/keepassx-csv-importer.ts @@ -30,6 +30,8 @@ export class KeePassXCsvImporter extends BaseImporter implements Importer { cipher.login.username = this.getValueOrDefault(value.Username); cipher.login.password = this.getValueOrDefault(value.Password); cipher.login.uris = this.makeUriArray(value.URL); + cipher.login.totp = this.getValueOrDefault(value.TOTP); + this.cleanupCipher(cipher); result.ciphers.push(cipher); }); From 496bc74b515655a91f05152462e853dc3b808bc9 Mon Sep 17 00:00:00 2001 From: Jared Snider <116684653+JaredSnider-Bitwarden@users.noreply.github.com> Date: Fri, 18 Oct 2024 15:08:08 -0400 Subject: [PATCH 7/7] PM-13820 - RegistrationStartSecondaryComp - add bitLink to link (#11626) --- .../registration-start-secondary.component.html | 3 ++- .../registration-start-secondary.component.ts | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/libs/auth/src/angular/registration/registration-start/registration-start-secondary.component.html b/libs/auth/src/angular/registration/registration-start/registration-start-secondary.component.html index c878724106b..f4d0767f7be 100644 --- a/libs/auth/src/angular/registration/registration-start/registration-start-secondary.component.html +++ b/libs/auth/src/angular/registration/registration-start/registration-start-secondary.component.html @@ -1,3 +1,4 @@ {{ "alreadyHaveAccount" | i18n }} {{ "logIn" | i18n }}{{ "alreadyHaveAccount" | i18n }} + {{ "logIn" | i18n }} diff --git a/libs/auth/src/angular/registration/registration-start/registration-start-secondary.component.ts b/libs/auth/src/angular/registration/registration-start/registration-start-secondary.component.ts index 1c2883beb08..f01a8c71bba 100644 --- a/libs/auth/src/angular/registration/registration-start/registration-start-secondary.component.ts +++ b/libs/auth/src/angular/registration/registration-start/registration-start-secondary.component.ts @@ -4,6 +4,7 @@ import { ActivatedRoute, RouterModule } from "@angular/router"; import { firstValueFrom } from "rxjs"; import { JslibModule } from "@bitwarden/angular/jslib.module"; +import { LinkModule } from "@bitwarden/components"; /** * RegistrationStartSecondaryComponentData @@ -17,7 +18,7 @@ export interface RegistrationStartSecondaryComponentData { standalone: true, selector: "auth-registration-start-secondary", templateUrl: "./registration-start-secondary.component.html", - imports: [CommonModule, JslibModule, RouterModule], + imports: [CommonModule, JslibModule, RouterModule, LinkModule], }) export class RegistrationStartSecondaryComponent implements OnInit { loginRoute: string;