From 78b7dd59f54a90ef0ab5156c0959fa545e024056 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Garci=CC=81a?= Date: Mon, 19 May 2025 16:05:05 +0200 Subject: [PATCH] [PM-19479] Client-Managed SDK state definition --- .../browser/src/background/main.background.ts | 1 + apps/browser/src/popup/app.component.ts | 24 ++++- .../service-container/service-container.ts | 1 + .../src/services/jslib-services.module.ts | 1 + .../services/sdk/default-sdk.service.ts | 100 +++++++++++++++++- 5 files changed, 125 insertions(+), 2 deletions(-) diff --git a/apps/browser/src/background/main.background.ts b/apps/browser/src/background/main.background.ts index a724f857cd1..965218efaee 100644 --- a/apps/browser/src/background/main.background.ts +++ b/apps/browser/src/background/main.background.ts @@ -775,6 +775,7 @@ export default class MainBackground { this.accountService, this.kdfConfigService, this.keyService, + this.stateProvider, ); this.passwordStrengthService = new PasswordStrengthService(); diff --git a/apps/browser/src/popup/app.component.ts b/apps/browser/src/popup/app.component.ts index 5f7fbc1fad7..c1172fe7727 100644 --- a/apps/browser/src/popup/app.component.ts +++ b/apps/browser/src/popup/app.component.ts @@ -3,7 +3,7 @@ import { ChangeDetectorRef, Component, NgZone, OnDestroy, OnInit, inject } from "@angular/core"; import { takeUntilDestroyed } from "@angular/core/rxjs-interop"; import { NavigationEnd, Router, RouterOutlet } from "@angular/router"; -import { Subject, takeUntil, firstValueFrom, concatMap, filter, tap } from "rxjs"; +import { Subject, takeUntil, firstValueFrom, concatMap, filter, tap, map } from "rxjs"; import { DeviceTrustToastService } from "@bitwarden/angular/auth/services/device-trust-toast.service.abstraction"; import { LogoutReason } from "@bitwarden/auth/common"; @@ -13,6 +13,7 @@ import { AuthenticationStatus } from "@bitwarden/common/auth/enums/authenticatio import { AnimationControlService } from "@bitwarden/common/platform/abstractions/animation-control.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; +import { SdkService } from "@bitwarden/common/platform/abstractions/sdk/sdk.service"; import { StateService } from "@bitwarden/common/platform/abstractions/state.service"; import { MessageListener } from "@bitwarden/common/platform/messaging"; import { UserId } from "@bitwarden/common/types/guid"; @@ -74,10 +75,27 @@ export class AppComponent implements OnInit, OnDestroy { private biometricsService: BiometricsService, private deviceTrustToastService: DeviceTrustToastService, private popupSizeService: PopupSizeService, + private sdkService: SdkService, ) { this.deviceTrustToastService.setupListeners$.pipe(takeUntilDestroyed()).subscribe(); } + /* eslint-disable no-console */ + async testSdkState() { + const userId = (await firstValueFrom(this.accountService.activeAccount$)).id; + await firstValueFrom( + this.sdkService.userClient$(userId).pipe( + map(async (clientRc) => { + using clientRef = clientRc.take(); + const client = clientRef.value; + + const ciphers = await client.vault().print_the_ciphers(); + console.log("Ciphers2: ", ciphers); + }), + ), + ); + } + async ngOnInit() { initPopupClosedListener(); @@ -200,6 +218,10 @@ export class AppComponent implements OnInit, OnDestroy { .subscribe((state) => { this.routerAnimations = state; }); + + this.testSdkState().catch((e) => { + console.error("Error in testSdkState()", e); + }); } ngOnDestroy(): void { diff --git a/apps/cli/src/service-container/service-container.ts b/apps/cli/src/service-container/service-container.ts index cdf6c4bbfda..10e283fa40f 100644 --- a/apps/cli/src/service-container/service-container.ts +++ b/apps/cli/src/service-container/service-container.ts @@ -599,6 +599,7 @@ export class ServiceContainer { this.accountService, this.kdfConfigService, this.keyService, + this.stateProvider, customUserAgent, ); diff --git a/libs/angular/src/services/jslib-services.module.ts b/libs/angular/src/services/jslib-services.module.ts index a8638efba18..56e48033265 100644 --- a/libs/angular/src/services/jslib-services.module.ts +++ b/libs/angular/src/services/jslib-services.module.ts @@ -1446,6 +1446,7 @@ const safeProviders: SafeProvider[] = [ AccountServiceAbstraction, KdfConfigService, KeyService, + StateProvider, ], }), safeProvider({ 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 5c381c7dd1b..36b49a9124f 100644 --- a/libs/common/src/platform/services/sdk/default-sdk.service.ts +++ b/libs/common/src/platform/services/sdk/default-sdk.service.ts @@ -12,13 +12,19 @@ import { of, takeWhile, throwIfEmpty, + firstValueFrom, } from "rxjs"; +import { CipherData } from "@bitwarden/common/vault/models/data/cipher.data"; +import { Cipher } from "@bitwarden/common/vault/models/domain/cipher"; +import { ENCRYPTED_CIPHERS } from "@bitwarden/common/vault/services/key-state/ciphers.state"; import { KeyService, KdfConfigService, KdfConfig, KdfType } from "@bitwarden/key-management"; import { BitwardenClient, ClientSettings, + Cipher as SdkCipher, DeviceType as SdkDeviceType, + Repository as SdkRepository, } from "@bitwarden/sdk-internal"; import { EncryptedOrganizationKeyData } from "../../../admin-console/models/data/encrypted-organization-key.data"; @@ -34,6 +40,7 @@ import { SdkService, UserNotLoggedInError } from "../../abstractions/sdk/sdk.ser import { compareValues } from "../../misc/compare-values"; import { Rc } from "../../misc/reference-counting/rc"; import { EncryptedString } from "../../models/domain/enc-string"; +import { StateProvider, UserKeyDefinition } from "../../state"; // A symbol that represents an overriden client that is explicitly set to undefined, // blocking the creation of an internal client for that user. @@ -66,6 +73,7 @@ export class DefaultSdkService implements SdkService { private accountService: AccountService, private kdfConfigService: KdfConfigService, private keyService: KeyService, + private stateProvider?: StateProvider, private userAgent: string | null = null, ) {} @@ -150,7 +158,15 @@ export class DefaultSdkService implements SdkService { const settings = this.toSettings(env); const client = await this.sdkClientFactory.createSdkClient(settings); - await this.initializeClient(client, account, kdfParams, privateKey, userKey, orgKeys); + await this.initializeClient( + userId, + client, + account, + kdfParams, + privateKey, + userKey, + orgKeys, + ); return client; }; @@ -180,6 +196,7 @@ export class DefaultSdkService implements SdkService { } private async initializeClient( + userId: UserId, client: BitwardenClient, account: AccountInfo, kdfParams: KdfConfig, @@ -214,6 +231,30 @@ export class DefaultSdkService implements SdkService { .map(([k, v]) => [k, v.key]), ), }); + + if (this.stateProvider) { + // Initialize the cipher store + client + .platform() + .repository() + .register_cipher_repository( + new RepositoryRecordImpl( + userId, + this.stateProvider, + ENCRYPTED_CIPHERS, + new CipherMapper(), + ), + ); + + try { + const result = await client.vault().print_the_ciphers(); + // eslint-disable-next-line no-console + console.log("Ciphers: " + result); + } catch (e) { + // eslint-disable-next-line no-console + console.error("Error printing ciphers: " + e); + } + } } private toSettings(env: Environment): ClientSettings { @@ -282,3 +323,60 @@ export class DefaultSdkService implements SdkService { } } } + +interface SdkMapper { + toSdk(value: ClientType): SdkType; + fromSdk(value: SdkType): ClientType; +} + +class RepositoryRecordImpl implements SdkRepository { + constructor( + private userId: UserId, + private stateProvider: StateProvider, + private userKeyDefinition: UserKeyDefinition>, + private mapper: SdkMapper, + ) {} + + async get(id: string): Promise { + const prov = this.stateProvider.getUser(this.userId, this.userKeyDefinition); + const data = await firstValueFrom(prov.state$.pipe(map((data) => data ?? {}))); + + const cipher = data[id]; + if (!cipher) { + return null; + } + + return this.mapper.toSdk(cipher); + } + async list(): Promise { + const prov = this.stateProvider.getUser(this.userId, this.userKeyDefinition); + const ciphers = await firstValueFrom(prov.state$.pipe(map((data) => data ?? {}))); + + return Object.values(ciphers).map((cipher) => this.mapper.toSdk(cipher)); + } + async set(id: string, value: SdkType): Promise { + const prov = this.stateProvider.getUser(this.userId, this.userKeyDefinition); + const ciphers = await firstValueFrom(prov.state$.pipe(map((data) => data ?? {}))); + ciphers[id] = this.mapper.fromSdk(value); + await prov.update(() => ciphers); + } + async remove(id: string): Promise { + const prov = this.stateProvider.getUser(this.userId, this.userKeyDefinition); + const ciphers = await firstValueFrom(prov.state$.pipe(map((data) => data ?? {}))); + if (!ciphers[id]) { + return; + } + delete ciphers[id]; + await prov.update(() => ciphers); + } +} + +class CipherMapper implements SdkMapper { + toSdk(value: CipherData): SdkCipher { + return new Cipher(value).toSdkCipher(); + } + + fromSdk(value: SdkCipher): CipherData { + throw new Error("Cipher.fromSdk is not implemented yet"); + } +}