1
0
mirror of https://github.com/bitwarden/browser synced 2025-12-06 00:13:28 +00:00

[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
This commit is contained in:
Bernd Schoolmann
2025-12-04 09:14:09 +01:00
committed by GitHub
parent 433c0ced32
commit 2ef84ca460
11 changed files with 110 additions and 34 deletions

View File

@@ -1096,6 +1096,9 @@ describe("KeyRotationService", () => {
mockKeyService.userSigningKey$.mockReturnValue( mockKeyService.userSigningKey$.mockReturnValue(
new BehaviorSubject(TEST_VECTOR_SIGNING_KEY_V2 as WrappedSigningKey), 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( mockSecurityStateService.accountSecurityState$.mockReturnValue(
new BehaviorSubject(TEST_VECTOR_SECURITY_STATE_V2 as SignedSecurityState), new BehaviorSubject(TEST_VECTOR_SECURITY_STATE_V2 as SignedSecurityState),
); );
@@ -1140,6 +1143,7 @@ describe("KeyRotationService", () => {
publicKeyEncryptionKeyPair: { publicKeyEncryptionKeyPair: {
wrappedPrivateKey: TEST_VECTOR_PRIVATE_KEY_V2, wrappedPrivateKey: TEST_VECTOR_PRIVATE_KEY_V2,
publicKey: Utils.fromB64ToArray(TEST_VECTOR_PUBLIC_KEY_V2) as UnsignedPublicKey, 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, signingKey: TEST_VECTOR_SIGNING_KEY_V2 as WrappedSigningKey,
securityState: TEST_VECTOR_SECURITY_STATE_V2 as SignedSecurityState, securityState: TEST_VECTOR_SECURITY_STATE_V2 as SignedSecurityState,

View File

@@ -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 { 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 { SecurityStateService } from "@bitwarden/common/key-management/security-state/abstractions/security-state.service";
import { import {
SignedPublicKey,
SignedSecurityState, SignedSecurityState,
UnsignedPublicKey, UnsignedPublicKey,
WrappedPrivateKey, WrappedPrivateKey,
@@ -308,9 +309,11 @@ export class UserKeyRotationService {
userId: asUuid(userId), userId: asUuid(userId),
kdfParams: kdfConfig.toSdkConfig(), kdfParams: kdfConfig.toSdkConfig(),
email: email, email: email,
privateKey: cryptographicStateParameters.publicKeyEncryptionKeyPair.wrappedPrivateKey, accountCryptographicState: {
signingKey: undefined, V1: {
securityState: undefined, private_key: cryptographicStateParameters.publicKeyEncryptionKeyPair.wrappedPrivateKey,
},
},
method: { method: {
decryptedKey: { decrypted_user_key: cryptographicStateParameters.userKey.toBase64() }, decryptedKey: { decrypted_user_key: cryptographicStateParameters.userKey.toBase64() },
}, },
@@ -334,9 +337,15 @@ export class UserKeyRotationService {
userId: asUuid(userId), userId: asUuid(userId),
kdfParams: kdfConfig.toSdkConfig(), kdfParams: kdfConfig.toSdkConfig(),
email: email, email: email,
privateKey: cryptographicStateParameters.publicKeyEncryptionKeyPair.wrappedPrivateKey, accountCryptographicState: {
signingKey: cryptographicStateParameters.signingKey, V2: {
securityState: cryptographicStateParameters.securityState, private_key: cryptographicStateParameters.publicKeyEncryptionKeyPair.wrappedPrivateKey,
signing_key: cryptographicStateParameters.signingKey,
security_state: cryptographicStateParameters.securityState,
signed_public_key:
cryptographicStateParameters.publicKeyEncryptionKeyPair.signedPublicKey,
},
},
method: { method: {
decryptedKey: { decrypted_user_key: cryptographicStateParameters.userKey.toBase64() }, decryptedKey: { decrypted_user_key: cryptographicStateParameters.userKey.toBase64() },
}, },
@@ -632,6 +641,10 @@ export class UserKeyRotationService {
this.securityStateService.accountSecurityState$(user.id), this.securityStateService.accountSecurityState$(user.id),
"User security state", "User security state",
); );
const signedPublicKey = await this.firstValueFromOrThrow(
this.keyService.userSignedPublicKey$(user.id),
"User signed public key",
);
return { return {
masterKeyKdfConfig, masterKeyKdfConfig,
@@ -642,6 +655,7 @@ export class UserKeyRotationService {
publicKeyEncryptionKeyPair: { publicKeyEncryptionKeyPair: {
wrappedPrivateKey: currentUserKeyWrappedPrivateKey, wrappedPrivateKey: currentUserKeyWrappedPrivateKey,
publicKey: publicKey, publicKey: publicKey,
signedPublicKey: signedPublicKey!,
}, },
signingKey: signingKey!, signingKey: signingKey!,
securityState: securityState!, securityState: securityState!,
@@ -679,6 +693,7 @@ export type V2CryptographicStateParameters = {
publicKeyEncryptionKeyPair: { publicKeyEncryptionKeyPair: {
wrappedPrivateKey: WrappedPrivateKey; wrappedPrivateKey: WrappedPrivateKey;
publicKey: UnsignedPublicKey; publicKey: UnsignedPublicKey;
signedPublicKey: SignedPublicKey;
}; };
signingKey: WrappedSigningKey; signingKey: WrappedSigningKey;
securityState: SignedSecurityState; securityState: SignedSecurityState;

View File

@@ -1,6 +1,10 @@
import { Opaque } from "type-fest"; 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. * A private key, encrypted with a symmetric key.
@@ -10,7 +14,7 @@ export type WrappedPrivateKey = Opaque<EncString, "WrappedPrivateKey">;
/** /**
* A public key, signed with the accounts signature key. * A public key, signed with the accounts signature key.
*/ */
export type SignedPublicKey = Opaque<string, "SignedPublicKey">; export type SignedPublicKey = Opaque<SdkSignedPublicKey, "SignedPublicKey">;
/** /**
* A public key in base64 encoded SPKI-DER * A public key in base64 encoded SPKI-DER
*/ */

View File

@@ -1,5 +1,5 @@
import { EncryptedString } from "../../../key-management/crypto/models/enc-string"; 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 { UserKey } from "../../../types/key";
import { SymmetricCryptoKey } from "../../models/domain/symmetric-crypto-key"; import { SymmetricCryptoKey } from "../../models/domain/symmetric-crypto-key";
import { CRYPTO_DISK, CRYPTO_MEMORY, UserKeyDefinition } from "../../state"; import { CRYPTO_DISK, CRYPTO_MEMORY, UserKeyDefinition } from "../../state";
@@ -35,3 +35,12 @@ export const USER_KEY_ENCRYPTED_SIGNING_KEY = new UserKeyDefinition<WrappedSigni
clearOn: ["logout"], clearOn: ["logout"],
}, },
); );
export const USER_SIGNED_PUBLIC_KEY = new UserKeyDefinition<SignedPublicKey>(
CRYPTO_DISK,
"userSignedPublicKey",
{
deserializer: (obj) => obj,
clearOn: ["logout"],
},
);

View File

@@ -105,6 +105,7 @@ describe("DefaultSdkService", () => {
.mockReturnValue(of("private-key" as EncryptedString)); .mockReturnValue(of("private-key" as EncryptedString));
keyService.encryptedOrgKeys$.calledWith(userId).mockReturnValue(of({})); keyService.encryptedOrgKeys$.calledWith(userId).mockReturnValue(of({}));
keyService.userSigningKey$.calledWith(userId).mockReturnValue(of(null)); keyService.userSigningKey$.calledWith(userId).mockReturnValue(of(null));
keyService.userSignedPublicKey$.calledWith(userId).mockReturnValue(of(null));
securityStateService.accountSecurityState$.calledWith(userId).mockReturnValue(of(null)); securityStateService.accountSecurityState$.calledWith(userId).mockReturnValue(of(null));
}); });

View File

@@ -16,6 +16,7 @@ import {
} from "rxjs"; } from "rxjs";
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; 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. // 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 // eslint-disable-next-line no-restricted-imports
import { KeyService, KdfConfigService, KdfConfig, KdfType } from "@bitwarden/key-management"; import { KeyService, KdfConfigService, KdfConfig, KdfType } from "@bitwarden/key-management";
@@ -24,15 +25,14 @@ import {
ClientSettings, ClientSettings,
TokenProvider, TokenProvider,
UnsignedSharedKey, UnsignedSharedKey,
WrappedAccountCryptographicState,
} from "@bitwarden/sdk-internal"; } from "@bitwarden/sdk-internal";
import { ApiService } from "../../../abstractions/api.service"; import { ApiService } from "../../../abstractions/api.service";
import { AccountInfo, AccountService } from "../../../auth/abstractions/account.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 { SecurityStateService } from "../../../key-management/security-state/abstractions/security-state.service";
import { SignedSecurityState, WrappedSigningKey } from "../../../key-management/types";
import { OrganizationId, UserId } from "../../../types/guid"; import { OrganizationId, UserId } from "../../../types/guid";
import { UserKey } from "../../../types/key";
import { Environment, EnvironmentService } from "../../abstractions/environment.service"; import { Environment, EnvironmentService } from "../../abstractions/environment.service";
import { PlatformUtilsService } from "../../abstractions/platform-utils.service"; import { PlatformUtilsService } from "../../abstractions/platform-utils.service";
import { SdkClientFactory } from "../../abstractions/sdk/sdk-client-factory"; import { SdkClientFactory } from "../../abstractions/sdk/sdk-client-factory";
@@ -174,6 +174,9 @@ export class DefaultSdkService implements SdkService {
const securityState$ = this.securityStateService const securityState$ = this.securityStateService
.accountSecurityState$(userId) .accountSecurityState$(userId)
.pipe(distinctUntilChanged(compareValues)); .pipe(distinctUntilChanged(compareValues));
const signedPublicKey$ = this.keyService
.userSignedPublicKey$(userId)
.pipe(distinctUntilChanged(compareValues));
const client$ = combineLatest([ const client$ = combineLatest([
this.environmentService.getEnvironment$(userId), this.environmentService.getEnvironment$(userId),
@@ -184,11 +187,22 @@ export class DefaultSdkService implements SdkService {
signingKey$, signingKey$,
orgKeys$, orgKeys$,
securityState$, securityState$,
signedPublicKey$,
SdkLoadService.Ready, // Makes sure we wait (once) for the SDK to be loaded SdkLoadService.Ready, // Makes sure we wait (once) for the SDK to be loaded
]).pipe( ]).pipe(
// switchMap is required to allow the clean-up logic to be executed when `combineLatest` emits a new value. // switchMap is required to allow the clean-up logic to be executed when `combineLatest` emits a new value.
switchMap( 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 // Create our own observable to be able to implement clean-up logic
return new Observable<Rc<PasswordManagerClient>>((subscriber) => { return new Observable<Rc<PasswordManagerClient>>((subscriber) => {
const createAndInitializeClient = async () => { const createAndInitializeClient = async () => {
@@ -202,15 +216,31 @@ export class DefaultSdkService implements SdkService {
settings, 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( await this.initializeClient(
userId, userId,
client, client,
account, account,
kdfParams, kdfParams,
privateKey,
userKey, userKey,
signingKey, accountCryptographicState,
securityState,
orgKeys, orgKeys,
); );
@@ -245,10 +275,8 @@ export class DefaultSdkService implements SdkService {
client: PasswordManagerClient, client: PasswordManagerClient,
account: AccountInfo, account: AccountInfo,
kdfParams: KdfConfig, kdfParams: KdfConfig,
privateKey: EncryptedString,
userKey: UserKey, userKey: UserKey,
signingKey: WrappedSigningKey | null, accountCryptographicState: WrappedAccountCryptographicState,
securityState: SignedSecurityState | null,
orgKeys: Record<OrganizationId, EncString>, orgKeys: Record<OrganizationId, EncString>,
) { ) {
await client.crypto().initialize_user_crypto({ await client.crypto().initialize_user_crypto({
@@ -265,9 +293,7 @@ export class DefaultSdkService implements SdkService {
parallelism: kdfParams.parallelism, parallelism: kdfParams.parallelism,
}, },
}, },
privateKey, accountCryptographicState: accountCryptographicState,
signingKey: signingKey || undefined,
securityState: securityState || undefined,
}); });
// We initialize the org crypto even if the org_keys are // We initialize the org crypto even if the org_keys are

View File

@@ -253,6 +253,10 @@ export class DefaultSyncService extends CoreSyncService {
response.accountKeys.securityState.securityState, response.accountKeys.securityState.securityState,
response.id, response.id,
); );
await this.keyService.setSignedPublicKey(
response.accountKeys.publicKeyEncryptionKeyPair.signedPublicKey,
response.id,
);
} }
} else { } else {
await this.keyService.setPrivateKey(response.privateKey, response.id); await this.keyService.setPrivateKey(response.privateKey, response.id);

View File

@@ -7,7 +7,7 @@ import {
EncryptedString, EncryptedString,
EncString, EncString,
} from "@bitwarden/common/key-management/crypto/models/enc-string"; } 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 { KeySuffixOptions, HashPurpose } from "@bitwarden/common/platform/enums";
import { SymmetricCryptoKey } from "@bitwarden/common/platform/models/domain/symmetric-crypto-key"; import { SymmetricCryptoKey } from "@bitwarden/common/platform/models/domain/symmetric-crypto-key";
import { OrganizationId, ProviderId, UserId } from "@bitwarden/common/types/guid"; 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 * @param userId The user id for the key
*/ */
abstract validateUserKey(key: UserKey, userId: UserId): Promise<boolean>; abstract validateUserKey(key: UserKey, userId: UserId): Promise<boolean>;
abstract setSignedPublicKey(signedPublicKey: SignedPublicKey, userId: UserId): Promise<void>;
abstract userSignedPublicKey$(userId: UserId): Observable<SignedPublicKey | null>;
} }

View File

@@ -28,7 +28,7 @@ import {
EncryptedString, EncryptedString,
} from "@bitwarden/common/key-management/crypto/models/enc-string"; } from "@bitwarden/common/key-management/crypto/models/enc-string";
import { InternalMasterPasswordServiceAbstraction } from "@bitwarden/common/key-management/master-password/abstractions/master-password.service.abstraction"; 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 { VaultTimeoutStringType } from "@bitwarden/common/key-management/vault-timeout";
import { VAULT_TIMEOUT } from "@bitwarden/common/key-management/vault-timeout/services/vault-timeout-settings.state"; import { VAULT_TIMEOUT } from "@bitwarden/common/key-management/vault-timeout/services/vault-timeout-settings.state";
import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
@@ -46,6 +46,7 @@ import {
USER_EVER_HAD_USER_KEY, USER_EVER_HAD_USER_KEY,
USER_KEY, USER_KEY,
USER_KEY_ENCRYPTED_SIGNING_KEY, USER_KEY_ENCRYPTED_SIGNING_KEY,
USER_SIGNED_PUBLIC_KEY,
} from "@bitwarden/common/platform/services/key-state/user-key.state"; } from "@bitwarden/common/platform/services/key-state/user-key.state";
import { StateProvider } from "@bitwarden/common/platform/state"; import { StateProvider } from "@bitwarden/common/platform/state";
import { CsprngArray } from "@bitwarden/common/types/csprng"; import { CsprngArray } from "@bitwarden/common/types/csprng";
@@ -1013,4 +1014,12 @@ export class DefaultKeyService implements KeyServiceAbstraction {
}), }),
); );
} }
async setSignedPublicKey(signedPublicKey: SignedPublicKey, userId: UserId): Promise<void> {
await this.stateProvider.setUserState(USER_SIGNED_PUBLIC_KEY, signedPublicKey, userId);
}
userSignedPublicKey$(userId: UserId): Observable<SignedPublicKey | null> {
return this.stateProvider.getUserState$(USER_SIGNED_PUBLIC_KEY, userId);
}
} }

16
package-lock.json generated
View File

@@ -23,8 +23,8 @@
"@angular/platform-browser": "20.3.15", "@angular/platform-browser": "20.3.15",
"@angular/platform-browser-dynamic": "20.3.15", "@angular/platform-browser-dynamic": "20.3.15",
"@angular/router": "20.3.15", "@angular/router": "20.3.15",
"@bitwarden/commercial-sdk-internal": "0.2.0-main.403", "@bitwarden/commercial-sdk-internal": "0.2.0-main.409",
"@bitwarden/sdk-internal": "0.2.0-main.403", "@bitwarden/sdk-internal": "0.2.0-main.409",
"@electron/fuses": "1.8.0", "@electron/fuses": "1.8.0",
"@emotion/css": "11.13.5", "@emotion/css": "11.13.5",
"@koa/multer": "4.0.0", "@koa/multer": "4.0.0",
@@ -4615,9 +4615,9 @@
"link": true "link": true
}, },
"node_modules/@bitwarden/commercial-sdk-internal": { "node_modules/@bitwarden/commercial-sdk-internal": {
"version": "0.2.0-main.403", "version": "0.2.0-main.409",
"resolved": "https://registry.npmjs.org/@bitwarden/commercial-sdk-internal/-/commercial-sdk-internal-0.2.0-main.403.tgz", "resolved": "https://registry.npmjs.org/@bitwarden/commercial-sdk-internal/-/commercial-sdk-internal-0.2.0-main.409.tgz",
"integrity": "sha512-M2ZUu29oua7CaDTNK7mCwY7PhaIUbNYogAAvxLOmkJuyHNxxqvS9usjjlD2CkQVNBeTUFqvAQpaZQo9vgzEEFA==", "integrity": "sha512-86AVuOG5S9Te9ZMvozCV1FN8v98ROnUwr24nTeocqD/5OJIKoWWXk1t+g4YoVF+8AItpD6hFfO/uL05mG80jDA==",
"license": "BITWARDEN SOFTWARE DEVELOPMENT KIT LICENSE AGREEMENT", "license": "BITWARDEN SOFTWARE DEVELOPMENT KIT LICENSE AGREEMENT",
"dependencies": { "dependencies": {
"type-fest": "^4.41.0" "type-fest": "^4.41.0"
@@ -4720,9 +4720,9 @@
"link": true "link": true
}, },
"node_modules/@bitwarden/sdk-internal": { "node_modules/@bitwarden/sdk-internal": {
"version": "0.2.0-main.403", "version": "0.2.0-main.409",
"resolved": "https://registry.npmjs.org/@bitwarden/sdk-internal/-/sdk-internal-0.2.0-main.403.tgz", "resolved": "https://registry.npmjs.org/@bitwarden/sdk-internal/-/sdk-internal-0.2.0-main.409.tgz",
"integrity": "sha512-ROEZdTbeKU68kDh9WYm9wKsLQD5jdTRclXLKl8x0aTj+Tx0nKyyXmLyUfOP+qh3EHIetij4jwPx2z3uS+7r8mg==", "integrity": "sha512-3e3hZenNos1xACJ2Bsq14RrzCnWdIilJuJhFwZYQBI6PQ+R38UGGQhGJDandKvQggfR8bDCY3re8x8n9G7MDiA==",
"license": "GPL-3.0", "license": "GPL-3.0",
"dependencies": { "dependencies": {
"type-fest": "^4.41.0" "type-fest": "^4.41.0"

View File

@@ -157,8 +157,8 @@
"@angular/platform-browser": "20.3.15", "@angular/platform-browser": "20.3.15",
"@angular/platform-browser-dynamic": "20.3.15", "@angular/platform-browser-dynamic": "20.3.15",
"@angular/router": "20.3.15", "@angular/router": "20.3.15",
"@bitwarden/sdk-internal": "0.2.0-main.403", "@bitwarden/sdk-internal": "0.2.0-main.409",
"@bitwarden/commercial-sdk-internal": "0.2.0-main.403", "@bitwarden/commercial-sdk-internal": "0.2.0-main.409",
"@electron/fuses": "1.8.0", "@electron/fuses": "1.8.0",
"@emotion/css": "11.13.5", "@emotion/css": "11.13.5",
"@koa/multer": "4.0.0", "@koa/multer": "4.0.0",