From ea45c5d3c0266abcafe73701d1f56e26788f0971 Mon Sep 17 00:00:00 2001 From: Bernd Schoolmann Date: Wed, 17 Dec 2025 22:04:08 +0100 Subject: [PATCH] [PM-27315] Add account cryptographic state service (#17589) * Update account init and save signed public key * Add account cryptographic state service * Fix build * Cleanup * Fix build * Fix import * Fix build on browser * Fix * Fix DI * Fix * Fix * Fix * Fix * Fix * Fix test * Fix desktop build * Fix * Address nits * Cleanup setting private key * Add tests * Add tests * Add test coverage * Relative imports * Fix web build * Cleanup setting of private key --- .../browser/src/background/main.background.ts | 7 + .../service-container/service-container.ts | 8 ++ .../src/app/services/services.module.ts | 2 + ...sktop-set-initial-password.service.spec.ts | 4 + .../desktop-set-initial-password.service.ts | 3 + .../web-set-initial-password.service.spec.ts | 4 + .../web-set-initial-password.service.ts | 3 + apps/web/src/app/core/core.module.ts | 2 + ...initial-password.service.implementation.ts | 10 ++ ...fault-set-initial-password.service.spec.ts | 14 ++ .../src/services/jslib-services.module.ts | 10 ++ .../auth-request-login.strategy.spec.ts | 43 +++++- .../auth-request-login.strategy.ts | 6 + .../login-strategies/login.strategy.spec.ts | 6 + .../common/login-strategies/login.strategy.ts | 2 + .../password-login.strategy.spec.ts | 45 +++++- .../password-login.strategy.ts | 6 + .../sso-login.strategy.spec.ts | 41 +++++- .../login-strategies/sso-login.strategy.ts | 7 + .../user-api-login.strategy.spec.ts | 38 +++++ .../user-api-login.strategy.ts | 6 + .../webauthn-login.strategy.spec.ts | 51 +++++++ .../webauthn-login.strategy.ts | 6 + .../login-strategy.service.spec.ts | 4 + .../login-strategy.service.ts | 3 + .../response/identity-token.response.spec.ts | 32 +++++ .../response/identity-token.response.ts | 7 + .../account-cryptographic-state.service.ts | 22 +++ ...ccount-cryptographic-state.service.spec.ts | 133 ++++++++++++++++++ ...ult-account-cryptographic-state.service.ts | 35 +++++ .../keys/response/private-keys.response.ts | 29 ++++ .../services/key-state/user-key.state.spec.ts | 6 +- .../sync/default-sync.service.spec.ts | 8 +- .../src/platform/sync/default-sync.service.ts | 14 +- 34 files changed, 607 insertions(+), 10 deletions(-) create mode 100644 libs/common/src/key-management/account-cryptography/account-cryptographic-state.service.ts create mode 100644 libs/common/src/key-management/account-cryptography/default-account-cryptographic-state.service.spec.ts create mode 100644 libs/common/src/key-management/account-cryptography/default-account-cryptographic-state.service.ts diff --git a/apps/browser/src/background/main.background.ts b/apps/browser/src/background/main.background.ts index 7b509380f6d..c224dcf581e 100644 --- a/apps/browser/src/background/main.background.ts +++ b/apps/browser/src/background/main.background.ts @@ -87,6 +87,8 @@ import { HibpApiService } from "@bitwarden/common/dirt/services/hibp-api.service import { PhishingDetectionSettingsService } from "@bitwarden/common/dirt/services/phishing-detection/phishing-detection-settings.service"; import { ClientType } from "@bitwarden/common/enums"; import { ProcessReloadServiceAbstraction } from "@bitwarden/common/key-management/abstractions/process-reload.service"; +import { AccountCryptographicStateService } from "@bitwarden/common/key-management/account-cryptography/account-cryptographic-state.service"; +import { DefaultAccountCryptographicStateService } from "@bitwarden/common/key-management/account-cryptography/default-account-cryptographic-state.service"; import { DefaultKeyGenerationService, KeyGenerationService, @@ -455,6 +457,7 @@ export default class MainBackground { syncServiceListener: SyncServiceListener; browserInitialInstallService: BrowserInitialInstallService; backgroundSyncService: BackgroundSyncService; + accountCryptographicStateService: AccountCryptographicStateService; webPushConnectionService: WorkerWebPushConnectionService | UnsupportedWebPushConnectionService; themeStateService: DefaultThemeStateService; @@ -1010,6 +1013,9 @@ export default class MainBackground { this.avatarService = new AvatarService(this.apiService, this.stateProvider); this.providerService = new ProviderService(this.stateProvider); + this.accountCryptographicStateService = new DefaultAccountCryptographicStateService( + this.stateProvider, + ); this.syncService = new DefaultSyncService( this.masterPasswordService, this.accountService, @@ -1037,6 +1043,7 @@ export default class MainBackground { this.stateProvider, this.securityStateService, this.kdfConfigService, + this.accountCryptographicStateService, ); this.syncServiceListener = new SyncServiceListener( diff --git a/apps/cli/src/service-container/service-container.ts b/apps/cli/src/service-container/service-container.ts index f22f7cb6a00..2f1e92d14fc 100644 --- a/apps/cli/src/service-container/service-container.ts +++ b/apps/cli/src/service-container/service-container.ts @@ -69,6 +69,7 @@ import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abs import { DefaultBillingAccountProfileStateService } from "@bitwarden/common/billing/services/account/billing-account-profile-state.service"; import { HibpApiService } from "@bitwarden/common/dirt/services/hibp-api.service"; import { ClientType } from "@bitwarden/common/enums"; +import { DefaultAccountCryptographicStateService } from "@bitwarden/common/key-management/account-cryptography/default-account-cryptographic-state.service"; import { DefaultKeyGenerationService, KeyGenerationService, @@ -334,6 +335,7 @@ export class ServiceContainer { masterPasswordUnlockService: MasterPasswordUnlockService; cipherArchiveService: CipherArchiveService; lockService: LockService; + private accountCryptographicStateService: DefaultAccountCryptographicStateService; constructor() { let p = null; @@ -717,6 +719,10 @@ export class ServiceContainer { this.accountService, ); + this.accountCryptographicStateService = new DefaultAccountCryptographicStateService( + this.stateProvider, + ); + this.loginStrategyService = new LoginStrategyService( this.accountService, this.masterPasswordService, @@ -744,6 +750,7 @@ export class ServiceContainer { this.kdfConfigService, this.taskSchedulerService, this.configService, + this.accountCryptographicStateService, ); this.restrictedItemTypesService = new RestrictedItemTypesService( @@ -879,6 +886,7 @@ export class ServiceContainer { this.stateProvider, this.securityStateService, this.kdfConfigService, + this.accountCryptographicStateService, ); this.totpService = new TotpService(this.sdkService); diff --git a/apps/desktop/src/app/services/services.module.ts b/apps/desktop/src/app/services/services.module.ts index 874a4d851da..382b680efbc 100644 --- a/apps/desktop/src/app/services/services.module.ts +++ b/apps/desktop/src/app/services/services.module.ts @@ -56,6 +56,7 @@ import { AutofillSettingsServiceAbstraction } from "@bitwarden/common/autofill/s import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions"; import { ClientType } from "@bitwarden/common/enums"; import { ProcessReloadServiceAbstraction } from "@bitwarden/common/key-management/abstractions/process-reload.service"; +import { AccountCryptographicStateService } from "@bitwarden/common/key-management/account-cryptography/account-cryptographic-state.service"; import { KeyGenerationService } from "@bitwarden/common/key-management/crypto"; import { CryptoFunctionService as CryptoFunctionServiceAbstraction } from "@bitwarden/common/key-management/crypto/abstractions/crypto-function.service"; import { EncryptService } from "@bitwarden/common/key-management/crypto/abstractions/encrypt.service"; @@ -416,6 +417,7 @@ const safeProviders: SafeProvider[] = [ OrganizationUserApiService, InternalUserDecryptionOptionsServiceAbstraction, MessagingServiceAbstraction, + AccountCryptographicStateService, ], }), safeProvider({ diff --git a/apps/desktop/src/app/services/set-initial-password/desktop-set-initial-password.service.spec.ts b/apps/desktop/src/app/services/set-initial-password/desktop-set-initial-password.service.spec.ts index 717af25a1dc..6b29a464e2c 100644 --- a/apps/desktop/src/app/services/set-initial-password/desktop-set-initial-password.service.spec.ts +++ b/apps/desktop/src/app/services/set-initial-password/desktop-set-initial-password.service.spec.ts @@ -15,6 +15,7 @@ import { ApiService } from "@bitwarden/common/abstractions/api.service"; import { OrganizationApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/organization/organization-api.service.abstraction"; import { MasterPasswordApiService } from "@bitwarden/common/auth/abstractions/master-password-api.service.abstraction"; import { SetPasswordRequest } from "@bitwarden/common/auth/models/request/set-password.request"; +import { AccountCryptographicStateService } from "@bitwarden/common/key-management/account-cryptography/account-cryptographic-state.service"; import { EncryptService } from "@bitwarden/common/key-management/crypto/abstractions/encrypt.service"; import { EncString } from "@bitwarden/common/key-management/crypto/models/enc-string"; import { InternalMasterPasswordServiceAbstraction } from "@bitwarden/common/key-management/master-password/abstractions/master-password.service.abstraction"; @@ -43,6 +44,7 @@ describe("DesktopSetInitialPasswordService", () => { let organizationUserApiService: MockProxy; let userDecryptionOptionsService: MockProxy; let messagingService: MockProxy; + let accountCryptographicStateService: MockProxy; beforeEach(() => { apiService = mock(); @@ -56,6 +58,7 @@ describe("DesktopSetInitialPasswordService", () => { organizationUserApiService = mock(); userDecryptionOptionsService = mock(); messagingService = mock(); + accountCryptographicStateService = mock(); sut = new DesktopSetInitialPasswordService( apiService, @@ -69,6 +72,7 @@ describe("DesktopSetInitialPasswordService", () => { organizationUserApiService, userDecryptionOptionsService, messagingService, + accountCryptographicStateService, ); }); diff --git a/apps/desktop/src/app/services/set-initial-password/desktop-set-initial-password.service.ts b/apps/desktop/src/app/services/set-initial-password/desktop-set-initial-password.service.ts index 8de7e73fafe..cedfa3fe589 100644 --- a/apps/desktop/src/app/services/set-initial-password/desktop-set-initial-password.service.ts +++ b/apps/desktop/src/app/services/set-initial-password/desktop-set-initial-password.service.ts @@ -9,6 +9,7 @@ import { InternalUserDecryptionOptionsServiceAbstraction } from "@bitwarden/auth import { ApiService } from "@bitwarden/common/abstractions/api.service"; import { OrganizationApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/organization/organization-api.service.abstraction"; import { MasterPasswordApiService } from "@bitwarden/common/auth/abstractions/master-password-api.service.abstraction"; +import { AccountCryptographicStateService } from "@bitwarden/common/key-management/account-cryptography/account-cryptographic-state.service"; import { EncryptService } from "@bitwarden/common/key-management/crypto/abstractions/encrypt.service"; import { InternalMasterPasswordServiceAbstraction } from "@bitwarden/common/key-management/master-password/abstractions/master-password.service.abstraction"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; @@ -32,6 +33,7 @@ export class DesktopSetInitialPasswordService protected organizationUserApiService: OrganizationUserApiService, protected userDecryptionOptionsService: InternalUserDecryptionOptionsServiceAbstraction, private messagingService: MessagingService, + protected accountCryptographicStateService: AccountCryptographicStateService, ) { super( apiService, @@ -44,6 +46,7 @@ export class DesktopSetInitialPasswordService organizationApiService, organizationUserApiService, userDecryptionOptionsService, + accountCryptographicStateService, ); } diff --git a/apps/web/src/app/auth/core/services/password-management/set-initial-password/web-set-initial-password.service.spec.ts b/apps/web/src/app/auth/core/services/password-management/set-initial-password/web-set-initial-password.service.spec.ts index 647c9ae83d9..1bbaa0ec236 100644 --- a/apps/web/src/app/auth/core/services/password-management/set-initial-password/web-set-initial-password.service.spec.ts +++ b/apps/web/src/app/auth/core/services/password-management/set-initial-password/web-set-initial-password.service.spec.ts @@ -16,6 +16,7 @@ import { OrganizationApiServiceAbstraction } from "@bitwarden/common/admin-conso import { MasterPasswordApiService } from "@bitwarden/common/auth/abstractions/master-password-api.service.abstraction"; import { SetPasswordRequest } from "@bitwarden/common/auth/models/request/set-password.request"; import { OrganizationInviteService } from "@bitwarden/common/auth/services/organization-invite/organization-invite.service"; +import { AccountCryptographicStateService } from "@bitwarden/common/key-management/account-cryptography/account-cryptographic-state.service"; import { EncryptService } from "@bitwarden/common/key-management/crypto/abstractions/encrypt.service"; import { EncString } from "@bitwarden/common/key-management/crypto/models/enc-string"; import { InternalMasterPasswordServiceAbstraction } from "@bitwarden/common/key-management/master-password/abstractions/master-password.service.abstraction"; @@ -45,6 +46,7 @@ describe("WebSetInitialPasswordService", () => { let userDecryptionOptionsService: MockProxy; let organizationInviteService: MockProxy; let routerService: MockProxy; + let accountCryptographicStateService: MockProxy; beforeEach(() => { apiService = mock(); @@ -59,6 +61,7 @@ describe("WebSetInitialPasswordService", () => { userDecryptionOptionsService = mock(); organizationInviteService = mock(); routerService = mock(); + accountCryptographicStateService = mock(); sut = new WebSetInitialPasswordService( apiService, @@ -73,6 +76,7 @@ describe("WebSetInitialPasswordService", () => { userDecryptionOptionsService, organizationInviteService, routerService, + accountCryptographicStateService, ); }); diff --git a/apps/web/src/app/auth/core/services/password-management/set-initial-password/web-set-initial-password.service.ts b/apps/web/src/app/auth/core/services/password-management/set-initial-password/web-set-initial-password.service.ts index 19ddbf5e260..303b9148e8e 100644 --- a/apps/web/src/app/auth/core/services/password-management/set-initial-password/web-set-initial-password.service.ts +++ b/apps/web/src/app/auth/core/services/password-management/set-initial-password/web-set-initial-password.service.ts @@ -10,6 +10,7 @@ import { ApiService } from "@bitwarden/common/abstractions/api.service"; import { OrganizationApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/organization/organization-api.service.abstraction"; import { MasterPasswordApiService } from "@bitwarden/common/auth/abstractions/master-password-api.service.abstraction"; import { OrganizationInviteService } from "@bitwarden/common/auth/services/organization-invite/organization-invite.service"; +import { AccountCryptographicStateService } from "@bitwarden/common/key-management/account-cryptography/account-cryptographic-state.service"; import { EncryptService } from "@bitwarden/common/key-management/crypto/abstractions/encrypt.service"; import { InternalMasterPasswordServiceAbstraction } from "@bitwarden/common/key-management/master-password/abstractions/master-password.service.abstraction"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; @@ -34,6 +35,7 @@ export class WebSetInitialPasswordService protected userDecryptionOptionsService: InternalUserDecryptionOptionsServiceAbstraction, private organizationInviteService: OrganizationInviteService, private routerService: RouterService, + protected accountCryptographicStateService: AccountCryptographicStateService, ) { super( apiService, @@ -46,6 +48,7 @@ export class WebSetInitialPasswordService organizationApiService, organizationUserApiService, userDecryptionOptionsService, + accountCryptographicStateService, ); } diff --git a/apps/web/src/app/core/core.module.ts b/apps/web/src/app/core/core.module.ts index fb42e19f863..bd557dc5947 100644 --- a/apps/web/src/app/core/core.module.ts +++ b/apps/web/src/app/core/core.module.ts @@ -66,6 +66,7 @@ import { OrganizationInviteService } from "@bitwarden/common/auth/services/organ import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions/account/billing-account-profile-state.service"; import { ClientType } from "@bitwarden/common/enums"; import { ProcessReloadServiceAbstraction } from "@bitwarden/common/key-management/abstractions/process-reload.service"; +import { AccountCryptographicStateService } from "@bitwarden/common/key-management/account-cryptography/account-cryptographic-state.service"; import { CryptoFunctionService } from "@bitwarden/common/key-management/crypto/abstractions/crypto-function.service"; import { EncryptService } from "@bitwarden/common/key-management/crypto/abstractions/encrypt.service"; import { InternalMasterPasswordServiceAbstraction } from "@bitwarden/common/key-management/master-password/abstractions/master-password.service.abstraction"; @@ -314,6 +315,7 @@ const safeProviders: SafeProvider[] = [ InternalUserDecryptionOptionsServiceAbstraction, OrganizationInviteService, RouterService, + AccountCryptographicStateService, ], }), safeProvider({ diff --git a/libs/angular/src/auth/password-management/set-initial-password/default-set-initial-password.service.implementation.ts b/libs/angular/src/auth/password-management/set-initial-password/default-set-initial-password.service.implementation.ts index df5220b5255..bd3f78b9290 100644 --- a/libs/angular/src/auth/password-management/set-initial-password/default-set-initial-password.service.implementation.ts +++ b/libs/angular/src/auth/password-management/set-initial-password/default-set-initial-password.service.implementation.ts @@ -15,6 +15,7 @@ import { MasterPasswordApiService } from "@bitwarden/common/auth/abstractions/ma import { ForceSetPasswordReason } from "@bitwarden/common/auth/models/domain/force-set-password-reason"; import { SetPasswordRequest } from "@bitwarden/common/auth/models/request/set-password.request"; import { UpdateTdeOffboardingPasswordRequest } from "@bitwarden/common/auth/models/request/update-tde-offboarding-password.request"; +import { AccountCryptographicStateService } from "@bitwarden/common/key-management/account-cryptography/account-cryptographic-state.service"; import { EncryptService } from "@bitwarden/common/key-management/crypto/abstractions/encrypt.service"; import { EncString } from "@bitwarden/common/key-management/crypto/models/enc-string"; import { InternalMasterPasswordServiceAbstraction } from "@bitwarden/common/key-management/master-password/abstractions/master-password.service.abstraction"; @@ -44,6 +45,7 @@ export class DefaultSetInitialPasswordService implements SetInitialPasswordServi protected organizationApiService: OrganizationApiServiceAbstraction, protected organizationUserApiService: OrganizationUserApiService, protected userDecryptionOptionsService: InternalUserDecryptionOptionsServiceAbstraction, + protected accountCryptographicStateService: AccountCryptographicStateService, ) {} async setInitialPassword( @@ -162,6 +164,14 @@ export class DefaultSetInitialPasswordService implements SetInitialPasswordServi throw new Error("encrypted private key not found. Could not set private key in state."); } await this.keyService.setPrivateKey(keyPair[1].encryptedString, userId); + await this.accountCryptographicStateService.setAccountCryptographicState( + { + V1: { + private_key: keyPair[1].encryptedString, + }, + }, + userId, + ); } await this.masterPasswordService.setMasterKeyHash(newLocalMasterKeyHash, userId); diff --git a/libs/angular/src/auth/password-management/set-initial-password/default-set-initial-password.service.spec.ts b/libs/angular/src/auth/password-management/set-initial-password/default-set-initial-password.service.spec.ts index 8b95090e776..cfea011d0d9 100644 --- a/libs/angular/src/auth/password-management/set-initial-password/default-set-initial-password.service.spec.ts +++ b/libs/angular/src/auth/password-management/set-initial-password/default-set-initial-password.service.spec.ts @@ -20,6 +20,7 @@ import { MasterPasswordApiService } from "@bitwarden/common/auth/abstractions/ma import { ForceSetPasswordReason } from "@bitwarden/common/auth/models/domain/force-set-password-reason"; import { SetPasswordRequest } from "@bitwarden/common/auth/models/request/set-password.request"; import { UpdateTdeOffboardingPasswordRequest } from "@bitwarden/common/auth/models/request/update-tde-offboarding-password.request"; +import { AccountCryptographicStateService } from "@bitwarden/common/key-management/account-cryptography/account-cryptographic-state.service"; import { EncryptService } from "@bitwarden/common/key-management/crypto/abstractions/encrypt.service"; import { EncryptedString, @@ -56,6 +57,7 @@ describe("DefaultSetInitialPasswordService", () => { let organizationApiService: MockProxy; let organizationUserApiService: MockProxy; let userDecryptionOptionsService: MockProxy; + let accountCryptographicStateService: MockProxy; let userId: UserId; let userKey: UserKey; @@ -73,6 +75,7 @@ describe("DefaultSetInitialPasswordService", () => { organizationApiService = mock(); organizationUserApiService = mock(); userDecryptionOptionsService = mock(); + accountCryptographicStateService = mock(); userId = "userId" as UserId; userKey = new SymmetricCryptoKey(new Uint8Array(64).buffer as CsprngArray) as UserKey; @@ -90,6 +93,7 @@ describe("DefaultSetInitialPasswordService", () => { organizationApiService, organizationUserApiService, userDecryptionOptionsService, + accountCryptographicStateService, ); }); @@ -386,6 +390,16 @@ describe("DefaultSetInitialPasswordService", () => { // Assert expect(masterPasswordApiService.setPassword).toHaveBeenCalledWith(setPasswordRequest); expect(keyService.setPrivateKey).toHaveBeenCalledWith(keyPair[1].encryptedString, userId); + expect( + accountCryptographicStateService.setAccountCryptographicState, + ).toHaveBeenCalledWith( + { + V1: { + private_key: keyPair[1].encryptedString as EncryptedString, + }, + }, + userId, + ); }); it("should set the local master key hash to state", async () => { diff --git a/libs/angular/src/services/jslib-services.module.ts b/libs/angular/src/services/jslib-services.module.ts index 6881862615d..ab6ca7295e3 100644 --- a/libs/angular/src/services/jslib-services.module.ts +++ b/libs/angular/src/services/jslib-services.module.ts @@ -168,6 +168,8 @@ import { OrganizationBillingService } from "@bitwarden/common/billing/services/o import { DefaultSubscriptionPricingService } from "@bitwarden/common/billing/services/subscription-pricing.service"; import { HibpApiService } from "@bitwarden/common/dirt/services/hibp-api.service"; import { ProcessReloadServiceAbstraction } from "@bitwarden/common/key-management/abstractions/process-reload.service"; +import { AccountCryptographicStateService } from "@bitwarden/common/key-management/account-cryptography/account-cryptographic-state.service"; +import { DefaultAccountCryptographicStateService } from "@bitwarden/common/key-management/account-cryptography/default-account-cryptographic-state.service"; import { DefaultKeyGenerationService, KeyGenerationService, @@ -572,6 +574,7 @@ const safeProviders: SafeProvider[] = [ KdfConfigService, TaskSchedulerService, ConfigService, + AccountCryptographicStateService, ], }), safeProvider({ @@ -894,8 +897,14 @@ const safeProviders: SafeProvider[] = [ StateProvider, SecurityStateService, KdfConfigService, + AccountCryptographicStateService, ], }), + safeProvider({ + provide: AccountCryptographicStateService, + useClass: DefaultAccountCryptographicStateService, + deps: [StateProvider], + }), safeProvider({ provide: BroadcasterService, useClass: DefaultBroadcasterService, @@ -1565,6 +1574,7 @@ const safeProviders: SafeProvider[] = [ OrganizationApiServiceAbstraction, OrganizationUserApiService, InternalUserDecryptionOptionsServiceAbstraction, + AccountCryptographicStateService, ], }), safeProvider({ diff --git a/libs/auth/src/common/login-strategies/auth-request-login.strategy.spec.ts b/libs/auth/src/common/login-strategies/auth-request-login.strategy.spec.ts index b536ae0dc4b..4703472d480 100644 --- a/libs/auth/src/common/login-strategies/auth-request-login.strategy.spec.ts +++ b/libs/auth/src/common/login-strategies/auth-request-login.strategy.spec.ts @@ -6,6 +6,7 @@ import { TokenService } from "@bitwarden/common/auth/abstractions/token.service" import { IdentityTokenResponse } from "@bitwarden/common/auth/models/response/identity-token.response"; import { TwoFactorService } from "@bitwarden/common/auth/two-factor"; import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions/account/billing-account-profile-state.service"; +import { AccountCryptographicStateService } from "@bitwarden/common/key-management/account-cryptography/account-cryptographic-state.service"; import { EncryptService } from "@bitwarden/common/key-management/crypto/abstractions/encrypt.service"; import { DeviceTrustServiceAbstraction } from "@bitwarden/common/key-management/device-trust/abstractions/device-trust.service.abstraction"; import { FakeMasterPasswordService } from "@bitwarden/common/key-management/master-password/services/fake-master-password.service"; @@ -22,7 +23,7 @@ import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/pl import { StateService } from "@bitwarden/common/platform/abstractions/state.service"; import { Utils } from "@bitwarden/common/platform/misc/utils"; import { SymmetricCryptoKey } from "@bitwarden/common/platform/models/domain/symmetric-crypto-key"; -import { FakeAccountService, mockAccountServiceWith } from "@bitwarden/common/spec"; +import { makeEncString, FakeAccountService, mockAccountServiceWith } from "@bitwarden/common/spec"; import { CsprngArray } from "@bitwarden/common/types/csprng"; import { UserId } from "@bitwarden/common/types/guid"; import { MasterKey, UserKey } from "@bitwarden/common/types/key"; @@ -57,6 +58,7 @@ describe("AuthRequestLoginStrategy", () => { let kdfConfigService: MockProxy; let environmentService: MockProxy; let configService: MockProxy; + let accountCryptographicStateService: MockProxy; const mockUserId = Utils.newGuid() as UserId; let accountService: FakeAccountService; @@ -94,6 +96,7 @@ describe("AuthRequestLoginStrategy", () => { kdfConfigService = mock(); environmentService = mock(); configService = mock(); + accountCryptographicStateService = mock(); accountService = mockAccountServiceWith(mockUserId); masterPasswordService = new FakeMasterPasswordService(); @@ -125,6 +128,7 @@ describe("AuthRequestLoginStrategy", () => { kdfConfigService, environmentService, configService, + accountCryptographicStateService, ); tokenResponse = identityTokenResponseFactory(); @@ -208,4 +212,41 @@ describe("AuthRequestLoginStrategy", () => { // trustDeviceIfRequired should be called expect(deviceTrustService.trustDeviceIfRequired).not.toHaveBeenCalled(); }); + + it("sets account cryptographic state when accountKeysResponseModel is present", async () => { + const accountKeysData = { + publicKeyEncryptionKeyPair: { + publicKey: "testPublicKey", + wrappedPrivateKey: "testPrivateKey", + }, + }; + + tokenResponse = identityTokenResponseFactory(); + tokenResponse.key = makeEncString("mockEncryptedUserKey"); + // Add accountKeysResponseModel to the response + (tokenResponse as any).accountKeysResponseModel = { + publicKeyEncryptionKeyPair: accountKeysData.publicKeyEncryptionKeyPair, + toWrappedAccountCryptographicState: jest.fn().mockReturnValue({ + V1: { + private_key: "testPrivateKey", + }, + }), + }; + + apiService.postIdentityToken.mockResolvedValue(tokenResponse); + masterPasswordService.masterKeySubject.next(decMasterKey); + masterPasswordService.mock.decryptUserKeyWithMasterKey.mockResolvedValue(decUserKey); + + await authRequestLoginStrategy.logIn(credentials); + + expect(accountCryptographicStateService.setAccountCryptographicState).toHaveBeenCalledTimes(1); + expect(accountCryptographicStateService.setAccountCryptographicState).toHaveBeenCalledWith( + { + V1: { + private_key: "testPrivateKey", + }, + }, + mockUserId, + ); + }); }); diff --git a/libs/auth/src/common/login-strategies/auth-request-login.strategy.ts b/libs/auth/src/common/login-strategies/auth-request-login.strategy.ts index 7337b6733f8..16af5fa77dc 100644 --- a/libs/auth/src/common/login-strategies/auth-request-login.strategy.ts +++ b/libs/auth/src/common/login-strategies/auth-request-login.strategy.ts @@ -128,6 +128,12 @@ export class AuthRequestLoginStrategy extends LoginStrategy { response.privateKey ?? (await this.createKeyPairForOldAccount(userId)), userId, ); + if (response.accountKeysResponseModel) { + await this.accountCryptographicStateService.setAccountCryptographicState( + response.accountKeysResponseModel.toWrappedAccountCryptographicState(), + userId, + ); + } } exportCache(): CacheData { diff --git a/libs/auth/src/common/login-strategies/login.strategy.spec.ts b/libs/auth/src/common/login-strategies/login.strategy.spec.ts index 4bf72b69e1f..113f9f3f0d9 100644 --- a/libs/auth/src/common/login-strategies/login.strategy.spec.ts +++ b/libs/auth/src/common/login-strategies/login.strategy.spec.ts @@ -17,6 +17,7 @@ import { MasterPasswordPolicyResponse } from "@bitwarden/common/auth/models/resp import { IUserDecryptionOptionsServerResponse } from "@bitwarden/common/auth/models/response/user-decryption-options/user-decryption-options.response"; import { TwoFactorService } from "@bitwarden/common/auth/two-factor"; import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions/account/billing-account-profile-state.service"; +import { AccountCryptographicStateService } from "@bitwarden/common/key-management/account-cryptography/account-cryptographic-state.service"; import { EncryptService } from "@bitwarden/common/key-management/crypto/abstractions/encrypt.service"; import { EncString } from "@bitwarden/common/key-management/crypto/models/enc-string"; import { FakeMasterPasswordService } from "@bitwarden/common/key-management/master-password/services/fake-master-password.service"; @@ -136,6 +137,7 @@ describe("LoginStrategy", () => { let kdfConfigService: MockProxy; let environmentService: MockProxy; let configService: MockProxy; + let accountCryptographicStateService: MockProxy; let passwordLoginStrategy: PasswordLoginStrategy; let credentials: PasswordLoginCredentials; @@ -162,6 +164,7 @@ describe("LoginStrategy", () => { billingAccountProfileStateService = mock(); environmentService = mock(); configService = mock(); + accountCryptographicStateService = mock(); vaultTimeoutSettingsService = mock(); @@ -192,6 +195,7 @@ describe("LoginStrategy", () => { kdfConfigService, environmentService, configService, + accountCryptographicStateService, ); credentials = new PasswordLoginCredentials(email, masterPassword); }); @@ -518,6 +522,7 @@ describe("LoginStrategy", () => { kdfConfigService, environmentService, configService, + accountCryptographicStateService, ); apiService.postIdentityToken.mockResolvedValue(identityTokenResponseFactory()); @@ -579,6 +584,7 @@ describe("LoginStrategy", () => { kdfConfigService, environmentService, configService, + accountCryptographicStateService, ); const result = await passwordLoginStrategy.logIn(credentials); diff --git a/libs/auth/src/common/login-strategies/login.strategy.ts b/libs/auth/src/common/login-strategies/login.strategy.ts index 90a36284d3b..16f5e1e4320 100644 --- a/libs/auth/src/common/login-strategies/login.strategy.ts +++ b/libs/auth/src/common/login-strategies/login.strategy.ts @@ -18,6 +18,7 @@ import { IdentityTokenResponse } from "@bitwarden/common/auth/models/response/id import { IdentityTwoFactorResponse } from "@bitwarden/common/auth/models/response/identity-two-factor.response"; import { TwoFactorService } from "@bitwarden/common/auth/two-factor"; import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions/account/billing-account-profile-state.service"; +import { AccountCryptographicStateService } from "@bitwarden/common/key-management/account-cryptography/account-cryptographic-state.service"; import { EncryptService } from "@bitwarden/common/key-management/crypto/abstractions/encrypt.service"; import { InternalMasterPasswordServiceAbstraction } from "@bitwarden/common/key-management/master-password/abstractions/master-password.service.abstraction"; import { @@ -89,6 +90,7 @@ export abstract class LoginStrategy { protected KdfConfigService: KdfConfigService, protected environmentService: EnvironmentService, protected configService: ConfigService, + protected accountCryptographicStateService: AccountCryptographicStateService, ) {} abstract exportCache(): CacheData; diff --git a/libs/auth/src/common/login-strategies/password-login.strategy.spec.ts b/libs/auth/src/common/login-strategies/password-login.strategy.spec.ts index 2287bfb43e3..4188b779d81 100644 --- a/libs/auth/src/common/login-strategies/password-login.strategy.spec.ts +++ b/libs/auth/src/common/login-strategies/password-login.strategy.spec.ts @@ -12,6 +12,7 @@ import { IdentityTwoFactorResponse } from "@bitwarden/common/auth/models/respons import { MasterPasswordPolicyResponse } from "@bitwarden/common/auth/models/response/master-password-policy.response"; import { TwoFactorService } from "@bitwarden/common/auth/two-factor"; import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions/account/billing-account-profile-state.service"; +import { AccountCryptographicStateService } from "@bitwarden/common/key-management/account-cryptography/account-cryptographic-state.service"; import { EncryptService } from "@bitwarden/common/key-management/crypto/abstractions/encrypt.service"; import { FakeMasterPasswordService } from "@bitwarden/common/key-management/master-password/services/fake-master-password.service"; import { @@ -28,7 +29,7 @@ import { StateService } from "@bitwarden/common/platform/abstractions/state.serv import { HashPurpose } from "@bitwarden/common/platform/enums"; import { Utils } from "@bitwarden/common/platform/misc/utils"; import { SymmetricCryptoKey } from "@bitwarden/common/platform/models/domain/symmetric-crypto-key"; -import { FakeAccountService, mockAccountServiceWith } from "@bitwarden/common/spec"; +import { FakeAccountService, makeEncString, mockAccountServiceWith } from "@bitwarden/common/spec"; import { PasswordStrengthServiceAbstraction, PasswordStrengthService, @@ -85,6 +86,7 @@ describe("PasswordLoginStrategy", () => { let kdfConfigService: MockProxy; let environmentService: MockProxy; let configService: MockProxy; + let accountCryptographicStateService: MockProxy; let passwordLoginStrategy: PasswordLoginStrategy; let credentials: PasswordLoginCredentials; @@ -113,6 +115,7 @@ describe("PasswordLoginStrategy", () => { kdfConfigService = mock(); environmentService = mock(); configService = mock(); + accountCryptographicStateService = mock(); appIdService.getAppId.mockResolvedValue(deviceId); tokenService.decodeAccessToken.mockResolvedValue({ @@ -153,6 +156,7 @@ describe("PasswordLoginStrategy", () => { kdfConfigService, environmentService, configService, + accountCryptographicStateService, ); credentials = new PasswordLoginCredentials(email, masterPassword); tokenResponse = identityTokenResponseFactory(masterPasswordPolicyResponse); @@ -392,4 +396,43 @@ describe("PasswordLoginStrategy", () => { ); expect(result.userId).toBe(userId); }); + + it("sets account cryptographic state when accountKeysResponseModel is present", async () => { + const accountKeysData = { + publicKeyEncryptionKeyPair: { + publicKey: "testPublicKey", + wrappedPrivateKey: "testPrivateKey", + }, + }; + + tokenResponse = identityTokenResponseFactory(); + tokenResponse.key = makeEncString("mockEncryptedUserKey"); + // Add accountKeysResponseModel to the response + (tokenResponse as any).accountKeysResponseModel = { + publicKeyEncryptionKeyPair: accountKeysData.publicKeyEncryptionKeyPair, + toWrappedAccountCryptographicState: jest.fn().mockReturnValue({ + V1: { + private_key: "testPrivateKey", + }, + }), + }; + + apiService.postIdentityToken.mockResolvedValue(tokenResponse); + masterPasswordService.masterKeySubject.next(masterKey); + masterPasswordService.mock.decryptUserKeyWithMasterKey.mockResolvedValue( + new SymmetricCryptoKey(new Uint8Array(64).buffer as CsprngArray) as UserKey, + ); + + await passwordLoginStrategy.logIn(credentials); + + expect(accountCryptographicStateService.setAccountCryptographicState).toHaveBeenCalledTimes(1); + expect(accountCryptographicStateService.setAccountCryptographicState).toHaveBeenCalledWith( + { + V1: { + private_key: "testPrivateKey", + }, + }, + userId, + ); + }); }); diff --git a/libs/auth/src/common/login-strategies/password-login.strategy.ts b/libs/auth/src/common/login-strategies/password-login.strategy.ts index 842a48e28cd..63fff52194b 100644 --- a/libs/auth/src/common/login-strategies/password-login.strategy.ts +++ b/libs/auth/src/common/login-strategies/password-login.strategy.ts @@ -156,6 +156,12 @@ export class PasswordLoginStrategy extends LoginStrategy { response.privateKey ?? (await this.createKeyPairForOldAccount(userId)), userId, ); + if (response.accountKeysResponseModel) { + await this.accountCryptographicStateService.setAccountCryptographicState( + response.accountKeysResponseModel.toWrappedAccountCryptographicState(), + userId, + ); + } } protected override encryptionKeyMigrationRequired(response: IdentityTokenResponse): boolean { diff --git a/libs/auth/src/common/login-strategies/sso-login.strategy.spec.ts b/libs/auth/src/common/login-strategies/sso-login.strategy.spec.ts index 3dbce6500a8..484beb785d3 100644 --- a/libs/auth/src/common/login-strategies/sso-login.strategy.spec.ts +++ b/libs/auth/src/common/login-strategies/sso-login.strategy.spec.ts @@ -10,6 +10,7 @@ import { IdentityTokenResponse } from "@bitwarden/common/auth/models/response/id import { IUserDecryptionOptionsServerResponse } from "@bitwarden/common/auth/models/response/user-decryption-options/user-decryption-options.response"; import { TwoFactorService } from "@bitwarden/common/auth/two-factor"; import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions/account/billing-account-profile-state.service"; +import { AccountCryptographicStateService } from "@bitwarden/common/key-management/account-cryptography/account-cryptographic-state.service"; import { EncryptService } from "@bitwarden/common/key-management/crypto/abstractions/encrypt.service"; import { EncryptedString } from "@bitwarden/common/key-management/crypto/models/enc-string"; import { DeviceTrustServiceAbstraction } from "@bitwarden/common/key-management/device-trust/abstractions/device-trust.service.abstraction"; @@ -30,7 +31,7 @@ import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/pl import { StateService } from "@bitwarden/common/platform/abstractions/state.service"; import { Utils } from "@bitwarden/common/platform/misc/utils"; import { SymmetricCryptoKey } from "@bitwarden/common/platform/models/domain/symmetric-crypto-key"; -import { FakeAccountService, mockAccountServiceWith } from "@bitwarden/common/spec"; +import { FakeAccountService, makeEncString, mockAccountServiceWith } from "@bitwarden/common/spec"; import { CsprngArray } from "@bitwarden/common/types/csprng"; import { UserId } from "@bitwarden/common/types/guid"; import { DeviceKey, MasterKey, UserKey } from "@bitwarden/common/types/key"; @@ -70,6 +71,7 @@ describe("SsoLoginStrategy", () => { let kdfConfigService: MockProxy; let environmentService: MockProxy; let configService: MockProxy; + let accountCryptographicStateService: MockProxy; let ssoLoginStrategy: SsoLoginStrategy; let credentials: SsoLoginCredentials; @@ -108,6 +110,7 @@ describe("SsoLoginStrategy", () => { kdfConfigService = mock(); environmentService = mock(); configService = mock(); + accountCryptographicStateService = mock(); tokenService.getTwoFactorToken.mockResolvedValue(null); appIdService.getAppId.mockResolvedValue(deviceId); @@ -162,6 +165,7 @@ describe("SsoLoginStrategy", () => { kdfConfigService, environmentService, configService, + accountCryptographicStateService, ); credentials = new SsoLoginCredentials(ssoCode, ssoCodeVerifier, ssoRedirectUrl, ssoOrgId); }); @@ -556,4 +560,39 @@ describe("SsoLoginStrategy", () => { expect(keyService.setUserKey).toHaveBeenCalledWith(userKey, userId); }); }); + + it("sets account cryptographic state when accountKeysResponseModel is present", async () => { + const accountKeysData = { + publicKeyEncryptionKeyPair: { + publicKey: "testPublicKey", + wrappedPrivateKey: "testPrivateKey", + }, + }; + + const tokenResponse = identityTokenResponseFactory(); + tokenResponse.key = makeEncString("mockEncryptedUserKey"); + // Add accountKeysResponseModel to the response + (tokenResponse as any).accountKeysResponseModel = { + publicKeyEncryptionKeyPair: accountKeysData.publicKeyEncryptionKeyPair, + toWrappedAccountCryptographicState: jest.fn().mockReturnValue({ + V1: { + private_key: "testPrivateKey", + }, + }), + }; + + apiService.postIdentityToken.mockResolvedValue(tokenResponse); + + await ssoLoginStrategy.logIn(credentials); + + expect(accountCryptographicStateService.setAccountCryptographicState).toHaveBeenCalledTimes(1); + expect(accountCryptographicStateService.setAccountCryptographicState).toHaveBeenCalledWith( + { + V1: { + private_key: "testPrivateKey", + }, + }, + userId, + ); + }); }); diff --git a/libs/auth/src/common/login-strategies/sso-login.strategy.ts b/libs/auth/src/common/login-strategies/sso-login.strategy.ts index 33802765aca..a9d60fca21a 100644 --- a/libs/auth/src/common/login-strategies/sso-login.strategy.ts +++ b/libs/auth/src/common/login-strategies/sso-login.strategy.ts @@ -339,6 +339,13 @@ export class SsoLoginStrategy extends LoginStrategy { tokenResponse: IdentityTokenResponse, userId: UserId, ): Promise { + if (tokenResponse.accountKeysResponseModel) { + await this.accountCryptographicStateService.setAccountCryptographicState( + tokenResponse.accountKeysResponseModel.toWrappedAccountCryptographicState(), + userId, + ); + } + if (tokenResponse.hasMasterKeyEncryptedUserKey()) { // User has masterKeyEncryptedUserKey, so set the userKeyEncryptedPrivateKey // Note: new JIT provisioned SSO users will not yet have a user asymmetric key pair diff --git a/libs/auth/src/common/login-strategies/user-api-login.strategy.spec.ts b/libs/auth/src/common/login-strategies/user-api-login.strategy.spec.ts index 9bf282dee11..02613e527ec 100644 --- a/libs/auth/src/common/login-strategies/user-api-login.strategy.spec.ts +++ b/libs/auth/src/common/login-strategies/user-api-login.strategy.spec.ts @@ -5,6 +5,7 @@ import { ApiService } from "@bitwarden/common/abstractions/api.service"; import { TokenService } from "@bitwarden/common/auth/abstractions/token.service"; import { TwoFactorService } from "@bitwarden/common/auth/two-factor"; import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions/account/billing-account-profile-state.service"; +import { AccountCryptographicStateService } from "@bitwarden/common/key-management/account-cryptography/account-cryptographic-state.service"; import { EncryptService } from "@bitwarden/common/key-management/crypto/abstractions/encrypt.service"; import { KeyConnectorService } from "@bitwarden/common/key-management/key-connector/abstractions/key-connector.service"; import { FakeMasterPasswordService } from "@bitwarden/common/key-management/master-password/services/fake-master-password.service"; @@ -58,6 +59,7 @@ describe("UserApiLoginStrategy", () => { let vaultTimeoutSettingsService: MockProxy; let kdfConfigService: MockProxy; let configService: MockProxy; + let accountCryptographicStateService: MockProxy; let apiLogInStrategy: UserApiLoginStrategy; let credentials: UserApiLoginCredentials; @@ -91,6 +93,7 @@ describe("UserApiLoginStrategy", () => { vaultTimeoutSettingsService = mock(); kdfConfigService = mock(); configService = mock(); + accountCryptographicStateService = mock(); appIdService.getAppId.mockResolvedValue(deviceId); tokenService.getTwoFactorToken.mockResolvedValue(null); @@ -119,6 +122,7 @@ describe("UserApiLoginStrategy", () => { kdfConfigService, environmentService, configService, + accountCryptographicStateService, ); credentials = new UserApiLoginCredentials(apiClientId, apiClientSecret); @@ -226,4 +230,38 @@ describe("UserApiLoginStrategy", () => { ); expect(keyService.setUserKey).toHaveBeenCalledWith(userKey, userId); }); + + it("sets account cryptographic state when accountKeysResponseModel is present", async () => { + const accountKeysData = { + publicKeyEncryptionKeyPair: { + publicKey: "testPublicKey", + wrappedPrivateKey: "testPrivateKey", + }, + }; + + const tokenResponse = identityTokenResponseFactory(); + // Add accountKeysResponseModel to the response + (tokenResponse as any).accountKeysResponseModel = { + publicKeyEncryptionKeyPair: accountKeysData.publicKeyEncryptionKeyPair, + toWrappedAccountCryptographicState: jest.fn().mockReturnValue({ + V1: { + private_key: "testPrivateKey", + }, + }), + }; + + apiService.postIdentityToken.mockResolvedValue(tokenResponse); + + await apiLogInStrategy.logIn(credentials); + + expect(accountCryptographicStateService.setAccountCryptographicState).toHaveBeenCalledTimes(1); + expect(accountCryptographicStateService.setAccountCryptographicState).toHaveBeenCalledWith( + { + V1: { + private_key: "testPrivateKey", + }, + }, + userId, + ); + }); }); diff --git a/libs/auth/src/common/login-strategies/user-api-login.strategy.ts b/libs/auth/src/common/login-strategies/user-api-login.strategy.ts index df532799576..c5a9110d63e 100644 --- a/libs/auth/src/common/login-strategies/user-api-login.strategy.ts +++ b/libs/auth/src/common/login-strategies/user-api-login.strategy.ts @@ -87,6 +87,12 @@ export class UserApiLoginStrategy extends LoginStrategy { response.privateKey ?? (await this.createKeyPairForOldAccount(userId)), userId, ); + if (response.accountKeysResponseModel) { + await this.accountCryptographicStateService.setAccountCryptographicState( + response.accountKeysResponseModel.toWrappedAccountCryptographicState(), + userId, + ); + } } // Overridden to save client ID and secret to token service diff --git a/libs/auth/src/common/login-strategies/webauthn-login.strategy.spec.ts b/libs/auth/src/common/login-strategies/webauthn-login.strategy.spec.ts index 9d664a7837c..2ae79f46d7c 100644 --- a/libs/auth/src/common/login-strategies/webauthn-login.strategy.spec.ts +++ b/libs/auth/src/common/login-strategies/webauthn-login.strategy.spec.ts @@ -9,6 +9,7 @@ import { IUserDecryptionOptionsServerResponse } from "@bitwarden/common/auth/mod import { WebAuthnLoginAssertionResponseRequest } from "@bitwarden/common/auth/services/webauthn-login/request/webauthn-login-assertion-response.request"; import { TwoFactorService } from "@bitwarden/common/auth/two-factor"; import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions/account/billing-account-profile-state.service"; +import { AccountCryptographicStateService } from "@bitwarden/common/key-management/account-cryptography/account-cryptographic-state.service"; import { EncryptService } from "@bitwarden/common/key-management/crypto/abstractions/encrypt.service"; import { FakeMasterPasswordService } from "@bitwarden/common/key-management/master-password/services/fake-master-password.service"; import { @@ -56,6 +57,7 @@ describe("WebAuthnLoginStrategy", () => { let kdfConfigService: MockProxy; let environmentService: MockProxy; let configService: MockProxy; + let accountCryptographicStateService: MockProxy; let webAuthnLoginStrategy!: WebAuthnLoginStrategy; @@ -101,6 +103,7 @@ describe("WebAuthnLoginStrategy", () => { kdfConfigService = mock(); environmentService = mock(); configService = mock(); + accountCryptographicStateService = mock(); tokenService.getTwoFactorToken.mockResolvedValue(null); appIdService.getAppId.mockResolvedValue(deviceId); @@ -128,6 +131,7 @@ describe("WebAuthnLoginStrategy", () => { kdfConfigService, environmentService, configService, + accountCryptographicStateService, ); // Create credentials @@ -340,6 +344,53 @@ describe("WebAuthnLoginStrategy", () => { // Assert expect(keyService.setUserKey).not.toHaveBeenCalled(); }); + + it("sets account cryptographic state when accountKeysResponseModel is present", async () => { + // Arrange + const accountKeysData = { + publicKeyEncryptionKeyPair: { + publicKey: "testPublicKey", + wrappedPrivateKey: "testPrivateKey", + }, + }; + + const idTokenResponse: IdentityTokenResponse = identityTokenResponseFactory( + null, + userDecryptionOptsServerResponseWithWebAuthnPrfOption, + ); + // Add accountKeysResponseModel to the response + (idTokenResponse as any).accountKeysResponseModel = { + publicKeyEncryptionKeyPair: accountKeysData.publicKeyEncryptionKeyPair, + toWrappedAccountCryptographicState: jest.fn().mockReturnValue({ + V1: { + private_key: "testPrivateKey", + }, + }), + }; + + apiService.postIdentityToken.mockResolvedValue(idTokenResponse); + + const mockPrfPrivateKey: Uint8Array = randomBytes(32); + const mockUserKeyArray: Uint8Array = randomBytes(32); + encryptService.unwrapDecapsulationKey.mockResolvedValue(mockPrfPrivateKey); + encryptService.decapsulateKeyUnsigned.mockResolvedValue( + new SymmetricCryptoKey(mockUserKeyArray), + ); + + // Act + await webAuthnLoginStrategy.logIn(webAuthnCredentials); + + // Assert + expect(accountCryptographicStateService.setAccountCryptographicState).toHaveBeenCalledTimes(1); + expect(accountCryptographicStateService.setAccountCryptographicState).toHaveBeenCalledWith( + { + V1: { + private_key: "testPrivateKey", + }, + }, + userId, + ); + }); }); // Helpers and mocks diff --git a/libs/auth/src/common/login-strategies/webauthn-login.strategy.ts b/libs/auth/src/common/login-strategies/webauthn-login.strategy.ts index dbce7628335..77a881b5964 100644 --- a/libs/auth/src/common/login-strategies/webauthn-login.strategy.ts +++ b/libs/auth/src/common/login-strategies/webauthn-login.strategy.ts @@ -107,6 +107,12 @@ export class WebAuthnLoginStrategy extends LoginStrategy { response.privateKey ?? (await this.createKeyPairForOldAccount(userId)), userId, ); + if (response.accountKeysResponseModel) { + await this.accountCryptographicStateService.setAccountCryptographicState( + response.accountKeysResponseModel.toWrappedAccountCryptographicState(), + userId, + ); + } } exportCache(): CacheData { diff --git a/libs/auth/src/common/services/login-strategies/login-strategy.service.spec.ts b/libs/auth/src/common/services/login-strategies/login-strategy.service.spec.ts index c7e18105a0d..a79d6bb0514 100644 --- a/libs/auth/src/common/services/login-strategies/login-strategy.service.spec.ts +++ b/libs/auth/src/common/services/login-strategies/login-strategy.service.spec.ts @@ -13,6 +13,7 @@ import { PreloginResponse } from "@bitwarden/common/auth/models/response/prelogi import { UserDecryptionOptionsResponse } from "@bitwarden/common/auth/models/response/user-decryption-options/user-decryption-options.response"; import { TwoFactorService } from "@bitwarden/common/auth/two-factor"; import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions/account/billing-account-profile-state.service"; +import { DefaultAccountCryptographicStateService } from "@bitwarden/common/key-management/account-cryptography/default-account-cryptographic-state.service"; import { EncryptService } from "@bitwarden/common/key-management/crypto/abstractions/encrypt.service"; import { DeviceTrustServiceAbstraction } from "@bitwarden/common/key-management/device-trust/abstractions/device-trust.service.abstraction"; import { KeyConnectorService } from "@bitwarden/common/key-management/key-connector/abstractions/key-connector.service"; @@ -84,6 +85,7 @@ describe("LoginStrategyService", () => { let kdfConfigService: MockProxy; let taskSchedulerService: MockProxy; let configService: MockProxy; + let accountCryptographicStateService: MockProxy; let stateProvider: FakeGlobalStateProvider; let loginStrategyCacheExpirationState: FakeGlobalState; @@ -117,6 +119,7 @@ describe("LoginStrategyService", () => { kdfConfigService = mock(); taskSchedulerService = mock(); configService = mock(); + accountCryptographicStateService = mock(); sut = new LoginStrategyService( accountService, @@ -145,6 +148,7 @@ describe("LoginStrategyService", () => { kdfConfigService, taskSchedulerService, configService, + accountCryptographicStateService, ); loginStrategyCacheExpirationState = stateProvider.getFake(CACHE_EXPIRATION_KEY); diff --git a/libs/auth/src/common/services/login-strategies/login-strategy.service.ts b/libs/auth/src/common/services/login-strategies/login-strategy.service.ts index 5720fce254e..5f8cd304a18 100644 --- a/libs/auth/src/common/services/login-strategies/login-strategy.service.ts +++ b/libs/auth/src/common/services/login-strategies/login-strategy.service.ts @@ -19,6 +19,7 @@ import { TokenTwoFactorRequest } from "@bitwarden/common/auth/models/request/ide import { TwoFactorService } from "@bitwarden/common/auth/two-factor"; import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions/account/billing-account-profile-state.service"; import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum"; +import { AccountCryptographicStateService } from "@bitwarden/common/key-management/account-cryptography/account-cryptographic-state.service"; import { EncryptService } from "@bitwarden/common/key-management/crypto/abstractions/encrypt.service"; import { DeviceTrustServiceAbstraction } from "@bitwarden/common/key-management/device-trust/abstractions/device-trust.service.abstraction"; import { KeyConnectorService } from "@bitwarden/common/key-management/key-connector/abstractions/key-connector.service"; @@ -160,6 +161,7 @@ export class LoginStrategyService implements LoginStrategyServiceAbstraction { protected kdfConfigService: KdfConfigService, protected taskSchedulerService: TaskSchedulerService, protected configService: ConfigService, + protected accountCryptographicStateService: AccountCryptographicStateService, ) { this.currentAuthnTypeState = this.stateProvider.get(CURRENT_LOGIN_STRATEGY_KEY); this.loginStrategyCacheState = this.stateProvider.get(CACHE_KEY); @@ -509,6 +511,7 @@ export class LoginStrategyService implements LoginStrategyServiceAbstraction { this.kdfConfigService, this.environmentService, this.configService, + this.accountCryptographicStateService, ]; return source.pipe( diff --git a/libs/common/src/auth/models/response/identity-token.response.spec.ts b/libs/common/src/auth/models/response/identity-token.response.spec.ts index 4f8454968dc..6a32b89f1df 100644 --- a/libs/common/src/auth/models/response/identity-token.response.spec.ts +++ b/libs/common/src/auth/models/response/identity-token.response.spec.ts @@ -116,4 +116,36 @@ describe("IdentityTokenResponse", () => { const identityTokenResponse = new IdentityTokenResponse(response); expect(identityTokenResponse.userDecryptionOptions).toBeDefined(); }); + + it("should create response with accountKeys not present", () => { + const response = { + access_token: accessToken, + token_type: tokenType, + AccountKeys: null as unknown, + }; + + const identityTokenResponse = new IdentityTokenResponse(response); + expect(identityTokenResponse.accountKeysResponseModel).toBeNull(); + }); + + it("should create response with accountKeys present", () => { + const accountKeysData = { + publicKeyEncryptionKeyPair: { + publicKey: "testPublicKey", + wrappedPrivateKey: "testPrivateKey", + }, + }; + + const response = { + access_token: accessToken, + token_type: tokenType, + AccountKeys: accountKeysData, + }; + + const identityTokenResponse = new IdentityTokenResponse(response); + expect(identityTokenResponse.accountKeysResponseModel).toBeDefined(); + expect( + identityTokenResponse.accountKeysResponseModel?.publicKeyEncryptionKeyPair, + ).toBeDefined(); + }); }); diff --git a/libs/common/src/auth/models/response/identity-token.response.ts b/libs/common/src/auth/models/response/identity-token.response.ts index 958f520a2c6..ae208ef1a36 100644 --- a/libs/common/src/auth/models/response/identity-token.response.ts +++ b/libs/common/src/auth/models/response/identity-token.response.ts @@ -5,6 +5,7 @@ import { Argon2KdfConfig, KdfConfig, KdfType, PBKDF2KdfConfig } from "@bitwarden/key-management"; import { EncString } from "../../../key-management/crypto/models/enc-string"; +import { PrivateKeysResponseModel } from "../../../key-management/keys/response/private-keys.response"; import { BaseResponse } from "../../../models/response/base.response"; import { MasterPasswordPolicyResponse } from "./master-password-policy.response"; @@ -19,6 +20,7 @@ export class IdentityTokenResponse extends BaseResponse { // Decryption Information privateKey: string; // userKeyEncryptedPrivateKey + accountKeysResponseModel: PrivateKeysResponseModel | null = null; key?: EncString; // masterKeyEncryptedUserKey twoFactorToken: string; kdfConfig: KdfConfig; @@ -52,6 +54,11 @@ export class IdentityTokenResponse extends BaseResponse { } this.privateKey = this.getResponseProperty("PrivateKey"); + if (this.getResponseProperty("AccountKeys") != null) { + this.accountKeysResponseModel = new PrivateKeysResponseModel( + this.getResponseProperty("AccountKeys"), + ); + } const key = this.getResponseProperty("Key"); if (key) { this.key = new EncString(key); diff --git a/libs/common/src/key-management/account-cryptography/account-cryptographic-state.service.ts b/libs/common/src/key-management/account-cryptography/account-cryptographic-state.service.ts new file mode 100644 index 00000000000..cc3306a849a --- /dev/null +++ b/libs/common/src/key-management/account-cryptography/account-cryptographic-state.service.ts @@ -0,0 +1,22 @@ +import { Observable } from "rxjs"; + +import { WrappedAccountCryptographicState } from "@bitwarden/sdk-internal"; +import { UserId } from "@bitwarden/user-core"; + +export abstract class AccountCryptographicStateService { + /** + * Emits the provided user's account cryptographic state or null if there is no account cryptographic state present for the user. + */ + abstract accountCryptographicState$( + userId: UserId, + ): Observable; + + /** + * Sets the account cryptographic state. + * This is not yet validated, and is only validated upon SDK initialization. + */ + abstract setAccountCryptographicState( + accountCryptographicState: WrappedAccountCryptographicState, + userId: UserId, + ): Promise; +} diff --git a/libs/common/src/key-management/account-cryptography/default-account-cryptographic-state.service.spec.ts b/libs/common/src/key-management/account-cryptography/default-account-cryptographic-state.service.spec.ts new file mode 100644 index 00000000000..62710073c00 --- /dev/null +++ b/libs/common/src/key-management/account-cryptography/default-account-cryptographic-state.service.spec.ts @@ -0,0 +1,133 @@ +import { firstValueFrom } from "rxjs"; + +import { WrappedAccountCryptographicState } from "@bitwarden/sdk-internal"; +import { FakeStateProvider } from "@bitwarden/state-test-utils"; +import { UserId } from "@bitwarden/user-core"; + +import { FakeAccountService, mockAccountServiceWith } from "../../../spec"; + +import { + ACCOUNT_CRYPTOGRAPHIC_STATE, + DefaultAccountCryptographicStateService, +} from "./default-account-cryptographic-state.service"; + +describe("DefaultAccountCryptographicStateService", () => { + let service: DefaultAccountCryptographicStateService; + let stateProvider: FakeStateProvider; + let accountService: FakeAccountService; + + const mockUserId = "user-id" as UserId; + + beforeEach(() => { + accountService = mockAccountServiceWith(mockUserId); + stateProvider = new FakeStateProvider(accountService); + service = new DefaultAccountCryptographicStateService(stateProvider); + }); + + describe("accountCryptographicState$", () => { + it("returns null when no state is set", async () => { + const result = await firstValueFrom(service.accountCryptographicState$(mockUserId)); + expect(result).toBeNull(); + }); + + it("returns the account cryptographic state when set (V1)", async () => { + const mockState: WrappedAccountCryptographicState = { + V1: { + private_key: "test-wrapped-state" as any, + }, + }; + await stateProvider.setUserState(ACCOUNT_CRYPTOGRAPHIC_STATE, mockState, mockUserId); + const result = await firstValueFrom(service.accountCryptographicState$(mockUserId)); + expect(result).toEqual(mockState); + }); + + it("returns the account cryptographic state when set (V2)", async () => { + const mockState: WrappedAccountCryptographicState = { + V2: { + private_key: "test-wrapped-private-key" as any, + signing_key: "test-wrapped-signing-key" as any, + signed_public_key: "test-signed-public-key" as any, + security_state: "test-security-state", + }, + }; + await stateProvider.setUserState(ACCOUNT_CRYPTOGRAPHIC_STATE, mockState, mockUserId); + const result = await firstValueFrom(service.accountCryptographicState$(mockUserId)); + expect(result).toEqual(mockState); + }); + + it("emits updated state when state changes", async () => { + const mockState1: any = { + V1: { + private_key: "test-state-1" as any, + }, + }; + const mockState2: any = { + V1: { + private_key: "test-state-2" as any, + }, + }; + + await stateProvider.setUserState(ACCOUNT_CRYPTOGRAPHIC_STATE, mockState1, mockUserId); + + const observable = service.accountCryptographicState$(mockUserId); + const results: (WrappedAccountCryptographicState | null)[] = []; + const subscription = observable.subscribe((state) => results.push(state)); + + await stateProvider.setUserState(ACCOUNT_CRYPTOGRAPHIC_STATE, mockState2, mockUserId); + + subscription.unsubscribe(); + + expect(results).toHaveLength(2); + expect(results[0]).toEqual(mockState1); + expect(results[1]).toEqual(mockState2); + }); + }); + + describe("setAccountCryptographicState", () => { + it("sets the account cryptographic state", async () => { + const mockState: WrappedAccountCryptographicState = { + V1: { + private_key: "test-wrapped-state" as any, + }, + }; + + await service.setAccountCryptographicState(mockState, mockUserId); + + const result = await firstValueFrom(service.accountCryptographicState$(mockUserId)); + + expect(result).toEqual(mockState); + }); + + it("overwrites existing state", async () => { + const mockState1: WrappedAccountCryptographicState = { + V1: { + private_key: "test-state-1" as any, + }, + }; + const mockState2: WrappedAccountCryptographicState = { + V1: { + private_key: "test-state-2" as any, + }, + }; + + await service.setAccountCryptographicState(mockState1, mockUserId); + await service.setAccountCryptographicState(mockState2, mockUserId); + + const result = await firstValueFrom(service.accountCryptographicState$(mockUserId)); + + expect(result).toEqual(mockState2); + }); + }); + + describe("ACCOUNT_CRYPTOGRAPHIC_STATE key definition", () => { + it("deserializer returns object as-is", () => { + const mockState: any = { + V1: { + private_key: "test" as any, + }, + }; + const result = ACCOUNT_CRYPTOGRAPHIC_STATE.deserializer(mockState); + expect(result).toBe(mockState); + }); + }); +}); diff --git a/libs/common/src/key-management/account-cryptography/default-account-cryptographic-state.service.ts b/libs/common/src/key-management/account-cryptography/default-account-cryptographic-state.service.ts new file mode 100644 index 00000000000..c37ac3c4fd1 --- /dev/null +++ b/libs/common/src/key-management/account-cryptography/default-account-cryptographic-state.service.ts @@ -0,0 +1,35 @@ +import { Observable } from "rxjs"; + +import { WrappedAccountCryptographicState } from "@bitwarden/sdk-internal"; +import { CRYPTO_DISK, StateProvider, UserKeyDefinition } from "@bitwarden/state"; +import { UserId } from "@bitwarden/user-core"; + +import { AccountCryptographicStateService } from "./account-cryptographic-state.service"; + +export const ACCOUNT_CRYPTOGRAPHIC_STATE = new UserKeyDefinition( + CRYPTO_DISK, + "accountCryptographicState", + { + deserializer: (obj) => obj as WrappedAccountCryptographicState, + clearOn: ["logout"], + }, +); + +export class DefaultAccountCryptographicStateService implements AccountCryptographicStateService { + constructor(protected stateProvider: StateProvider) {} + + accountCryptographicState$(userId: UserId): Observable { + return this.stateProvider.getUserState$(ACCOUNT_CRYPTOGRAPHIC_STATE, userId); + } + + async setAccountCryptographicState( + accountCryptographicState: WrappedAccountCryptographicState, + userId: UserId, + ): Promise { + await this.stateProvider.setUserState( + ACCOUNT_CRYPTOGRAPHIC_STATE, + accountCryptographicState, + userId, + ); + } +} diff --git a/libs/common/src/key-management/keys/response/private-keys.response.ts b/libs/common/src/key-management/keys/response/private-keys.response.ts index 2bd723fb455..acf3cc1423a 100644 --- a/libs/common/src/key-management/keys/response/private-keys.response.ts +++ b/libs/common/src/key-management/keys/response/private-keys.response.ts @@ -1,3 +1,5 @@ +import { SignedPublicKey, WrappedAccountCryptographicState } from "@bitwarden/sdk-internal"; + import { SecurityStateResponse } from "../../security-state/response/security-state.response"; import { PublicKeyEncryptionKeyPairResponse } from "./public-key-encryption-key-pair.response"; @@ -52,4 +54,31 @@ export class PrivateKeysResponseModel { ); } } + + toWrappedAccountCryptographicState(): WrappedAccountCryptographicState { + if (this.signatureKeyPair === null && this.securityState === null) { + // V1 user + return { + V1: { + private_key: this.publicKeyEncryptionKeyPair.wrappedPrivateKey, + }, + }; + } else if (this.signatureKeyPair !== null && this.securityState !== null) { + // V2 user + return { + V2: { + private_key: this.publicKeyEncryptionKeyPair.wrappedPrivateKey, + signing_key: this.signatureKeyPair.wrappedSigningKey, + signed_public_key: this.publicKeyEncryptionKeyPair.signedPublicKey as SignedPublicKey, + security_state: this.securityState.securityState as string, + }, + }; + } else { + throw new Error("Both signatureKeyPair and securityState must be present or absent together"); + } + } + + isV2Encryption(): boolean { + return this.signatureKeyPair !== null && this.securityState !== null; + } } 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 0af83ccf88a..2ea3c31bc1b 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,4 +1,4 @@ -import { EncString } from "../../../key-management/crypto/models/enc-string"; +import { EncryptedString, EncString } from "../../../key-management/crypto/models/enc-string"; import { EncryptionType } from "../../enums"; import { Utils } from "../../misc/utils"; @@ -27,7 +27,9 @@ describe("Encrypted private key", () => { it("should deserialize encrypted private key", () => { const encryptedPrivateKey = makeEncString().encryptedString; - const result = sut.deserializer(JSON.parse(JSON.stringify(encryptedPrivateKey))); + const result = sut.deserializer( + JSON.parse(JSON.stringify(encryptedPrivateKey as unknown)) as unknown as EncryptedString, + ); expect(result).toEqual(encryptedPrivateKey); }); diff --git a/libs/common/src/platform/sync/default-sync.service.spec.ts b/libs/common/src/platform/sync/default-sync.service.spec.ts index 621033ced65..fc83954ee7d 100644 --- a/libs/common/src/platform/sync/default-sync.service.spec.ts +++ b/libs/common/src/platform/sync/default-sync.service.spec.ts @@ -11,8 +11,6 @@ import { UserDecryptionOptions, UserDecryptionOptionsServiceAbstraction, } from "@bitwarden/auth/common"; -import { EncString } from "@bitwarden/common/key-management/crypto/models/enc-string"; -import { SecurityStateService } from "@bitwarden/common/key-management/security-state/abstractions/security-state.service"; // This import has been flagged as unallowed for this class. It may be involved in a circular dependency loop. // eslint-disable-next-line no-restricted-imports import { KdfConfigService, KeyService, PBKDF2KdfConfig } from "@bitwarden/key-management"; @@ -29,6 +27,8 @@ import { TokenService } from "../../auth/abstractions/token.service"; import { AuthenticationStatus } from "../../auth/enums/authentication-status"; import { DomainSettingsService } from "../../autofill/services/domain-settings.service"; import { BillingAccountProfileStateService } from "../../billing/abstractions"; +import { AccountCryptographicStateService } from "../../key-management/account-cryptography/account-cryptographic-state.service"; +import { EncString } from "../../key-management/crypto/models/enc-string"; import { KeyConnectorService } from "../../key-management/key-connector/abstractions/key-connector.service"; import { InternalMasterPasswordServiceAbstraction } from "../../key-management/master-password/abstractions/master-password.service.abstraction"; import { @@ -36,6 +36,7 @@ import { MasterPasswordSalt, MasterPasswordUnlockData, } from "../../key-management/master-password/types/master-password.types"; +import { SecurityStateService } from "../../key-management/security-state/abstractions/security-state.service"; import { SendApiService } from "../../tools/send/services/send-api.service.abstraction"; import { InternalSendService } from "../../tools/send/services/send.service.abstraction"; import { UserId } from "../../types/guid"; @@ -76,6 +77,7 @@ describe("DefaultSyncService", () => { let stateProvider: MockProxy; let securityStateService: MockProxy; let kdfConfigService: MockProxy; + let accountCryptographicStateService: MockProxy; let sut: DefaultSyncService; @@ -107,6 +109,7 @@ describe("DefaultSyncService", () => { stateProvider = mock(); securityStateService = mock(); kdfConfigService = mock(); + accountCryptographicStateService = mock(); sut = new DefaultSyncService( masterPasswordAbstraction, @@ -135,6 +138,7 @@ describe("DefaultSyncService", () => { stateProvider, securityStateService, kdfConfigService, + accountCryptographicStateService, ); }); diff --git a/libs/common/src/platform/sync/default-sync.service.ts b/libs/common/src/platform/sync/default-sync.service.ts index 8d2ccaffa18..49fd33b8035 100644 --- a/libs/common/src/platform/sync/default-sync.service.ts +++ b/libs/common/src/platform/sync/default-sync.service.ts @@ -9,8 +9,9 @@ import { CollectionDetailsResponse, CollectionService, } from "@bitwarden/admin-console/common"; -// This import has been flagged as unallowed for this class. It may be involved in a circular dependency loop. +import { AccountCryptographicStateService } from "@bitwarden/common/key-management/account-cryptography/account-cryptographic-state.service"; import { SecurityStateService } from "@bitwarden/common/key-management/security-state/abstractions/security-state.service"; +// This import has been flagged as unallowed for this class. It may be involved in a circular dependency loop. // eslint-disable-next-line no-restricted-imports import { KdfConfigService, KeyService } from "@bitwarden/key-management"; @@ -101,6 +102,7 @@ export class DefaultSyncService extends CoreSyncService { stateProvider: StateProvider, private securityStateService: SecurityStateService, private kdfConfigService: KdfConfigService, + private accountCryptographicStateService: AccountCryptographicStateService, ) { super( tokenService, @@ -239,12 +241,18 @@ export class DefaultSyncService extends CoreSyncService { // Cleanup: Only the first branch should be kept after the server always returns accountKeys https://bitwarden.atlassian.net/browse/PM-21768 if (response.accountKeys != null) { + await this.accountCryptographicStateService.setAccountCryptographicState( + response.accountKeys.toWrappedAccountCryptographicState(), + response.id, + ); + + // V1 and V2 users await this.keyService.setPrivateKey( response.accountKeys.publicKeyEncryptionKeyPair.wrappedPrivateKey, response.id, ); - if (response.accountKeys.signatureKeyPair !== null) { - // User is V2 user + // V2 users only + if (response.accountKeys.isV2Encryption()) { await this.keyService.setUserSigningKey( response.accountKeys.signatureKeyPair.wrappedSigningKey, response.id,