diff --git a/apps/browser/src/background/main.background.ts b/apps/browser/src/background/main.background.ts
index 819bb2a59f..a3dd1c473a 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/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 ae66d14d3f..88475d7dad 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 {
diff --git a/apps/cli/src/service-container/service-container.ts b/apps/cli/src/service-container/service-container.ts
index 8f8f1fa456..f3d71462f6 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/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 46e1fae80d..e1839517ff 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 @@
-
-
+
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 0000000000..0b81e0bd21
--- /dev/null
+++ b/apps/web/src/app/auth/trial-initiation/content/logo-company-testimonial.component.html
@@ -0,0 +1,28 @@
+
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 0000000000..9d9c447182
--- /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 9a7ed7e429..464c00c4a3 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 0000000000..3434e1d416
--- /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 0000000000..af474fdf84
--- /dev/null
+++ b/apps/web/src/images/register-layout/pcmag-logo.svg
@@ -0,0 +1,16 @@
+
\ No newline at end of file
diff --git a/libs/angular/src/services/jslib-services.module.ts b/libs/angular/src/services/jslib-services.module.ts
index 6355df5bf3..dfa8576e54 100644
--- a/libs/angular/src/services/jslib-services.module.ts
+++ b/libs/angular/src/services/jslib-services.module.ts
@@ -1347,6 +1347,9 @@ const safeProviders: SafeProvider[] = [
SdkClientFactory,
EnvironmentService,
PlatformUtilsServiceAbstraction,
+ AccountServiceAbstraction,
+ KdfConfigServiceAbstraction,
+ CryptoServiceAbstraction,
ApiServiceAbstraction,
],
}),
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 be9d8abe5b..3a6d26ef93 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",
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 c878724106..f4d0767f7b 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 1c2883beb0..f01a8c71bb 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;
diff --git a/libs/common/src/auth/abstractions/kdf-config.service.ts b/libs/common/src/auth/abstractions/kdf-config.service.ts
index 6b41979e1b..f4ffe31baa 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 cfd2a3e1de..604a186d76 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 020cfb8175..0a554f6249 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 360f2e91a7..5e4e4cb4cb 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 6b2afdb980..a6db9a2c1b 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 0000000000..dad99401f7
--- /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 d4a9cfeb7e..1b7a9a939a 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/libs/components/src/icon/icon.mdx b/libs/components/src/icon/icon.mdx
index d8b881b7e8..fc1c4cd3d5 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.
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 0000000000..0b3d729d9d
--- /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 0000000000..99eb99b993
--- /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 4047a49d57..03aa18cecb 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);
});
diff --git a/package-lock.json b/package-lock.json
index 2dcfda7388..c679767699 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 402b7c482d..38440adf92 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",