From 2ef84ca460e92db9cdd12f6079a215976d0b5ab1 Mon Sep 17 00:00:00 2001 From: Bernd Schoolmann Date: Thu, 4 Dec 2025 09:14:09 +0100 Subject: [PATCH] [PM-27230] Resolve sdk breaking changes; update account init and save signed public key (#17488) * Update account init and save signed public key * Update sdk * Fix build * Fix types * Fix test * Fix test --- .../user-key-rotation.service.spec.ts | 4 ++ .../key-rotation/user-key-rotation.service.ts | 27 +++++++--- libs/common/src/key-management/types.ts | 8 ++- .../services/key-state/user-key.state.ts | 11 +++- .../services/sdk/default-sdk.service.spec.ts | 1 + .../services/sdk/default-sdk.service.ts | 52 ++++++++++++++----- .../src/platform/sync/default-sync.service.ts | 4 ++ .../src/abstractions/key.service.ts | 6 ++- libs/key-management/src/key.service.ts | 11 +++- package-lock.json | 16 +++--- package.json | 4 +- 11 files changed, 110 insertions(+), 34 deletions(-) diff --git a/apps/web/src/app/key-management/key-rotation/user-key-rotation.service.spec.ts b/apps/web/src/app/key-management/key-rotation/user-key-rotation.service.spec.ts index b790fb8409..f4b50b4a77 100644 --- a/apps/web/src/app/key-management/key-rotation/user-key-rotation.service.spec.ts +++ b/apps/web/src/app/key-management/key-rotation/user-key-rotation.service.spec.ts @@ -1096,6 +1096,9 @@ describe("KeyRotationService", () => { mockKeyService.userSigningKey$.mockReturnValue( new BehaviorSubject(TEST_VECTOR_SIGNING_KEY_V2 as WrappedSigningKey), ); + mockKeyService.userSignedPublicKey$.mockReturnValue( + new BehaviorSubject(TEST_VECTOR_SIGNED_PUBLIC_KEY_V2 as SignedPublicKey), + ); mockSecurityStateService.accountSecurityState$.mockReturnValue( new BehaviorSubject(TEST_VECTOR_SECURITY_STATE_V2 as SignedSecurityState), ); @@ -1140,6 +1143,7 @@ describe("KeyRotationService", () => { publicKeyEncryptionKeyPair: { wrappedPrivateKey: TEST_VECTOR_PRIVATE_KEY_V2, publicKey: Utils.fromB64ToArray(TEST_VECTOR_PUBLIC_KEY_V2) as UnsignedPublicKey, + signedPublicKey: TEST_VECTOR_SIGNED_PUBLIC_KEY_V2 as SignedPublicKey, }, signingKey: TEST_VECTOR_SIGNING_KEY_V2 as WrappedSigningKey, securityState: TEST_VECTOR_SECURITY_STATE_V2 as SignedSecurityState, diff --git a/apps/web/src/app/key-management/key-rotation/user-key-rotation.service.ts b/apps/web/src/app/key-management/key-rotation/user-key-rotation.service.ts index 168dbe7442..b9bd23b12d 100644 --- a/apps/web/src/app/key-management/key-rotation/user-key-rotation.service.ts +++ b/apps/web/src/app/key-management/key-rotation/user-key-rotation.service.ts @@ -10,6 +10,7 @@ import { EncString } from "@bitwarden/common/key-management/crypto/models/enc-st import { DeviceTrustServiceAbstraction } from "@bitwarden/common/key-management/device-trust/abstractions/device-trust.service.abstraction"; import { SecurityStateService } from "@bitwarden/common/key-management/security-state/abstractions/security-state.service"; import { + SignedPublicKey, SignedSecurityState, UnsignedPublicKey, WrappedPrivateKey, @@ -308,9 +309,11 @@ export class UserKeyRotationService { userId: asUuid(userId), kdfParams: kdfConfig.toSdkConfig(), email: email, - privateKey: cryptographicStateParameters.publicKeyEncryptionKeyPair.wrappedPrivateKey, - signingKey: undefined, - securityState: undefined, + accountCryptographicState: { + V1: { + private_key: cryptographicStateParameters.publicKeyEncryptionKeyPair.wrappedPrivateKey, + }, + }, method: { decryptedKey: { decrypted_user_key: cryptographicStateParameters.userKey.toBase64() }, }, @@ -334,9 +337,15 @@ export class UserKeyRotationService { userId: asUuid(userId), kdfParams: kdfConfig.toSdkConfig(), email: email, - privateKey: cryptographicStateParameters.publicKeyEncryptionKeyPair.wrappedPrivateKey, - signingKey: cryptographicStateParameters.signingKey, - securityState: cryptographicStateParameters.securityState, + accountCryptographicState: { + V2: { + private_key: cryptographicStateParameters.publicKeyEncryptionKeyPair.wrappedPrivateKey, + signing_key: cryptographicStateParameters.signingKey, + security_state: cryptographicStateParameters.securityState, + signed_public_key: + cryptographicStateParameters.publicKeyEncryptionKeyPair.signedPublicKey, + }, + }, method: { decryptedKey: { decrypted_user_key: cryptographicStateParameters.userKey.toBase64() }, }, @@ -632,6 +641,10 @@ export class UserKeyRotationService { this.securityStateService.accountSecurityState$(user.id), "User security state", ); + const signedPublicKey = await this.firstValueFromOrThrow( + this.keyService.userSignedPublicKey$(user.id), + "User signed public key", + ); return { masterKeyKdfConfig, @@ -642,6 +655,7 @@ export class UserKeyRotationService { publicKeyEncryptionKeyPair: { wrappedPrivateKey: currentUserKeyWrappedPrivateKey, publicKey: publicKey, + signedPublicKey: signedPublicKey!, }, signingKey: signingKey!, securityState: securityState!, @@ -679,6 +693,7 @@ export type V2CryptographicStateParameters = { publicKeyEncryptionKeyPair: { wrappedPrivateKey: WrappedPrivateKey; publicKey: UnsignedPublicKey; + signedPublicKey: SignedPublicKey; }; signingKey: WrappedSigningKey; securityState: SignedSecurityState; diff --git a/libs/common/src/key-management/types.ts b/libs/common/src/key-management/types.ts index df64a3ed34..1c349e83a0 100644 --- a/libs/common/src/key-management/types.ts +++ b/libs/common/src/key-management/types.ts @@ -1,6 +1,10 @@ import { Opaque } from "type-fest"; -import { EncString, SignedSecurityState as SdkSignedSecurityState } from "@bitwarden/sdk-internal"; +import { + EncString, + SignedSecurityState as SdkSignedSecurityState, + SignedPublicKey as SdkSignedPublicKey, +} from "@bitwarden/sdk-internal"; /** * A private key, encrypted with a symmetric key. @@ -10,7 +14,7 @@ export type WrappedPrivateKey = Opaque; /** * A public key, signed with the accounts signature key. */ -export type SignedPublicKey = Opaque; +export type SignedPublicKey = Opaque; /** * A public key in base64 encoded SPKI-DER */ diff --git a/libs/common/src/platform/services/key-state/user-key.state.ts b/libs/common/src/platform/services/key-state/user-key.state.ts index 2416c211d6..64577768c8 100644 --- a/libs/common/src/platform/services/key-state/user-key.state.ts +++ b/libs/common/src/platform/services/key-state/user-key.state.ts @@ -1,5 +1,5 @@ import { EncryptedString } from "../../../key-management/crypto/models/enc-string"; -import { WrappedSigningKey } from "../../../key-management/types"; +import { SignedPublicKey, WrappedSigningKey } from "../../../key-management/types"; import { UserKey } from "../../../types/key"; import { SymmetricCryptoKey } from "../../models/domain/symmetric-crypto-key"; import { CRYPTO_DISK, CRYPTO_MEMORY, UserKeyDefinition } from "../../state"; @@ -35,3 +35,12 @@ export const USER_KEY_ENCRYPTED_SIGNING_KEY = new UserKeyDefinition( + CRYPTO_DISK, + "userSignedPublicKey", + { + deserializer: (obj) => obj, + clearOn: ["logout"], + }, +); 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 index dc94559407..1286ea7b7f 100644 --- a/libs/common/src/platform/services/sdk/default-sdk.service.spec.ts +++ b/libs/common/src/platform/services/sdk/default-sdk.service.spec.ts @@ -105,6 +105,7 @@ describe("DefaultSdkService", () => { .mockReturnValue(of("private-key" as EncryptedString)); keyService.encryptedOrgKeys$.calledWith(userId).mockReturnValue(of({})); keyService.userSigningKey$.calledWith(userId).mockReturnValue(of(null)); + keyService.userSignedPublicKey$.calledWith(userId).mockReturnValue(of(null)); securityStateService.accountSecurityState$.calledWith(userId).mockReturnValue(of(null)); }); 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 6e7bcbb197..5084f5f5f1 100644 --- a/libs/common/src/platform/services/sdk/default-sdk.service.ts +++ b/libs/common/src/platform/services/sdk/default-sdk.service.ts @@ -16,6 +16,7 @@ import { } from "rxjs"; import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; +import { UserKey } from "@bitwarden/common/types/key"; // 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 { KeyService, KdfConfigService, KdfConfig, KdfType } from "@bitwarden/key-management"; @@ -24,15 +25,14 @@ import { ClientSettings, TokenProvider, UnsignedSharedKey, + WrappedAccountCryptographicState, } from "@bitwarden/sdk-internal"; import { ApiService } from "../../../abstractions/api.service"; import { AccountInfo, AccountService } from "../../../auth/abstractions/account.service"; -import { EncryptedString, EncString } from "../../../key-management/crypto/models/enc-string"; +import { EncString } from "../../../key-management/crypto/models/enc-string"; import { SecurityStateService } from "../../../key-management/security-state/abstractions/security-state.service"; -import { SignedSecurityState, WrappedSigningKey } from "../../../key-management/types"; import { OrganizationId, UserId } from "../../../types/guid"; -import { UserKey } from "../../../types/key"; import { Environment, EnvironmentService } from "../../abstractions/environment.service"; import { PlatformUtilsService } from "../../abstractions/platform-utils.service"; import { SdkClientFactory } from "../../abstractions/sdk/sdk-client-factory"; @@ -174,6 +174,9 @@ export class DefaultSdkService implements SdkService { const securityState$ = this.securityStateService .accountSecurityState$(userId) .pipe(distinctUntilChanged(compareValues)); + const signedPublicKey$ = this.keyService + .userSignedPublicKey$(userId) + .pipe(distinctUntilChanged(compareValues)); const client$ = combineLatest([ this.environmentService.getEnvironment$(userId), @@ -184,11 +187,22 @@ export class DefaultSdkService implements SdkService { signingKey$, orgKeys$, securityState$, + signedPublicKey$, SdkLoadService.Ready, // Makes sure we wait (once) for the SDK to be loaded ]).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, signingKey, orgKeys, securityState]) => { + ([ + env, + account, + kdfParams, + privateKey, + userKey, + signingKey, + orgKeys, + securityState, + signedPublicKey, + ]) => { // Create our own observable to be able to implement clean-up logic return new Observable>((subscriber) => { const createAndInitializeClient = async () => { @@ -202,15 +216,31 @@ export class DefaultSdkService implements SdkService { settings, ); + let accountCryptographicState: WrappedAccountCryptographicState; + if (signingKey != null && securityState != null && signedPublicKey != null) { + accountCryptographicState = { + V2: { + private_key: privateKey, + signing_key: signingKey, + security_state: securityState, + signed_public_key: signedPublicKey, + }, + }; + } else { + accountCryptographicState = { + V1: { + private_key: privateKey, + }, + }; + } + await this.initializeClient( userId, client, account, kdfParams, - privateKey, userKey, - signingKey, - securityState, + accountCryptographicState, orgKeys, ); @@ -245,10 +275,8 @@ export class DefaultSdkService implements SdkService { client: PasswordManagerClient, account: AccountInfo, kdfParams: KdfConfig, - privateKey: EncryptedString, userKey: UserKey, - signingKey: WrappedSigningKey | null, - securityState: SignedSecurityState | null, + accountCryptographicState: WrappedAccountCryptographicState, orgKeys: Record, ) { await client.crypto().initialize_user_crypto({ @@ -265,9 +293,7 @@ export class DefaultSdkService implements SdkService { parallelism: kdfParams.parallelism, }, }, - privateKey, - signingKey: signingKey || undefined, - securityState: securityState || undefined, + accountCryptographicState: accountCryptographicState, }); // We initialize the org crypto even if the org_keys are diff --git a/libs/common/src/platform/sync/default-sync.service.ts b/libs/common/src/platform/sync/default-sync.service.ts index e599fbc1c4..910702bddd 100644 --- a/libs/common/src/platform/sync/default-sync.service.ts +++ b/libs/common/src/platform/sync/default-sync.service.ts @@ -253,6 +253,10 @@ export class DefaultSyncService extends CoreSyncService { response.accountKeys.securityState.securityState, response.id, ); + await this.keyService.setSignedPublicKey( + response.accountKeys.publicKeyEncryptionKeyPair.signedPublicKey, + response.id, + ); } } else { await this.keyService.setPrivateKey(response.privateKey, response.id); diff --git a/libs/key-management/src/abstractions/key.service.ts b/libs/key-management/src/abstractions/key.service.ts index f86ca922c9..feb4a38ac2 100644 --- a/libs/key-management/src/abstractions/key.service.ts +++ b/libs/key-management/src/abstractions/key.service.ts @@ -7,7 +7,7 @@ import { EncryptedString, EncString, } from "@bitwarden/common/key-management/crypto/models/enc-string"; -import { WrappedSigningKey } from "@bitwarden/common/key-management/types"; +import { SignedPublicKey, WrappedSigningKey } from "@bitwarden/common/key-management/types"; import { KeySuffixOptions, HashPurpose } from "@bitwarden/common/platform/enums"; import { SymmetricCryptoKey } from "@bitwarden/common/platform/models/domain/symmetric-crypto-key"; import { OrganizationId, ProviderId, UserId } from "@bitwarden/common/types/guid"; @@ -428,4 +428,8 @@ export abstract class KeyService { * @param userId The user id for the key */ abstract validateUserKey(key: UserKey, userId: UserId): Promise; + + abstract setSignedPublicKey(signedPublicKey: SignedPublicKey, userId: UserId): Promise; + + abstract userSignedPublicKey$(userId: UserId): Observable; } diff --git a/libs/key-management/src/key.service.ts b/libs/key-management/src/key.service.ts index f2c3c0eff7..f0e5f6ee08 100644 --- a/libs/key-management/src/key.service.ts +++ b/libs/key-management/src/key.service.ts @@ -28,7 +28,7 @@ import { EncryptedString, } from "@bitwarden/common/key-management/crypto/models/enc-string"; import { InternalMasterPasswordServiceAbstraction } from "@bitwarden/common/key-management/master-password/abstractions/master-password.service.abstraction"; -import { WrappedSigningKey } from "@bitwarden/common/key-management/types"; +import { SignedPublicKey, WrappedSigningKey } from "@bitwarden/common/key-management/types"; import { VaultTimeoutStringType } from "@bitwarden/common/key-management/vault-timeout"; import { VAULT_TIMEOUT } from "@bitwarden/common/key-management/vault-timeout/services/vault-timeout-settings.state"; import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; @@ -46,6 +46,7 @@ import { USER_EVER_HAD_USER_KEY, USER_KEY, USER_KEY_ENCRYPTED_SIGNING_KEY, + USER_SIGNED_PUBLIC_KEY, } from "@bitwarden/common/platform/services/key-state/user-key.state"; import { StateProvider } from "@bitwarden/common/platform/state"; import { CsprngArray } from "@bitwarden/common/types/csprng"; @@ -1013,4 +1014,12 @@ export class DefaultKeyService implements KeyServiceAbstraction { }), ); } + + async setSignedPublicKey(signedPublicKey: SignedPublicKey, userId: UserId): Promise { + await this.stateProvider.setUserState(USER_SIGNED_PUBLIC_KEY, signedPublicKey, userId); + } + + userSignedPublicKey$(userId: UserId): Observable { + return this.stateProvider.getUserState$(USER_SIGNED_PUBLIC_KEY, userId); + } } diff --git a/package-lock.json b/package-lock.json index 381515007a..88754d75ad 100644 --- a/package-lock.json +++ b/package-lock.json @@ -23,8 +23,8 @@ "@angular/platform-browser": "20.3.15", "@angular/platform-browser-dynamic": "20.3.15", "@angular/router": "20.3.15", - "@bitwarden/commercial-sdk-internal": "0.2.0-main.403", - "@bitwarden/sdk-internal": "0.2.0-main.403", + "@bitwarden/commercial-sdk-internal": "0.2.0-main.409", + "@bitwarden/sdk-internal": "0.2.0-main.409", "@electron/fuses": "1.8.0", "@emotion/css": "11.13.5", "@koa/multer": "4.0.0", @@ -4615,9 +4615,9 @@ "link": true }, "node_modules/@bitwarden/commercial-sdk-internal": { - "version": "0.2.0-main.403", - "resolved": "https://registry.npmjs.org/@bitwarden/commercial-sdk-internal/-/commercial-sdk-internal-0.2.0-main.403.tgz", - "integrity": "sha512-M2ZUu29oua7CaDTNK7mCwY7PhaIUbNYogAAvxLOmkJuyHNxxqvS9usjjlD2CkQVNBeTUFqvAQpaZQo9vgzEEFA==", + "version": "0.2.0-main.409", + "resolved": "https://registry.npmjs.org/@bitwarden/commercial-sdk-internal/-/commercial-sdk-internal-0.2.0-main.409.tgz", + "integrity": "sha512-86AVuOG5S9Te9ZMvozCV1FN8v98ROnUwr24nTeocqD/5OJIKoWWXk1t+g4YoVF+8AItpD6hFfO/uL05mG80jDA==", "license": "BITWARDEN SOFTWARE DEVELOPMENT KIT LICENSE AGREEMENT", "dependencies": { "type-fest": "^4.41.0" @@ -4720,9 +4720,9 @@ "link": true }, "node_modules/@bitwarden/sdk-internal": { - "version": "0.2.0-main.403", - "resolved": "https://registry.npmjs.org/@bitwarden/sdk-internal/-/sdk-internal-0.2.0-main.403.tgz", - "integrity": "sha512-ROEZdTbeKU68kDh9WYm9wKsLQD5jdTRclXLKl8x0aTj+Tx0nKyyXmLyUfOP+qh3EHIetij4jwPx2z3uS+7r8mg==", + "version": "0.2.0-main.409", + "resolved": "https://registry.npmjs.org/@bitwarden/sdk-internal/-/sdk-internal-0.2.0-main.409.tgz", + "integrity": "sha512-3e3hZenNos1xACJ2Bsq14RrzCnWdIilJuJhFwZYQBI6PQ+R38UGGQhGJDandKvQggfR8bDCY3re8x8n9G7MDiA==", "license": "GPL-3.0", "dependencies": { "type-fest": "^4.41.0" diff --git a/package.json b/package.json index 209bbc3beb..b07d9f2a9e 100644 --- a/package.json +++ b/package.json @@ -157,8 +157,8 @@ "@angular/platform-browser": "20.3.15", "@angular/platform-browser-dynamic": "20.3.15", "@angular/router": "20.3.15", - "@bitwarden/sdk-internal": "0.2.0-main.403", - "@bitwarden/commercial-sdk-internal": "0.2.0-main.403", + "@bitwarden/sdk-internal": "0.2.0-main.409", + "@bitwarden/commercial-sdk-internal": "0.2.0-main.409", "@electron/fuses": "1.8.0", "@emotion/css": "11.13.5", "@koa/multer": "4.0.0",