1
0
mirror of https://github.com/bitwarden/browser synced 2025-12-11 13:53:34 +00:00

[PM-21033/PM-22863] User Encryption v2 (#14942)

* Add new encrypt service functions

* Undo changes

* Cleanup

* Fix build

* Fix comments

* Switch encrypt service to use SDK functions

* Move remaining functions to PureCrypto

* Tests

* Increase test coverage

* Split up userkey rotation v2 and add tests

* Fix eslint

* Fix type errors

* Fix tests

* Implement signing keys

* Fix sdk init

* Remove key rotation v2 flag

* Fix parsing when user does not have signing keys

* Clear up trusted key naming

* Split up getNewAccountKeys

* Add trim and lowercase

* Replace user.email with masterKeySalt

* Add wasTrustDenied to verifyTrust in key rotation service

* Move testable userkey rotation service code to testable class

* Fix build

* Add comments

* Undo changes

* Fix incorrect behavior on aborting key rotation and fix import

* Fix tests

* Make members of userkey rotation service protected

* Fix type error

* Cleanup and add injectable annotation

* Fix tests

* Update apps/web/src/app/key-management/key-rotation/user-key-rotation.service.ts

Co-authored-by: Thomas Avery <43214426+Thomas-Avery@users.noreply.github.com>

* Remove v1 rotation request

* Add upgrade to user encryption v2

* Fix types

* Update sdk method calls

* Update request models for new server api for rotation

* Fix build

* Update userkey rotation for new server API

* Update crypto client call for new sdk changes

* Fix rotation with signing keys

* Cargo lock

* Fix userkey rotation service

* Fix types

* Undo changes to feature flag service

* Fix linting

* [PM-22863] Account security state (#15309)

* Add account security state

* Update key rotation

* Rename

* Fix build

* Cleanup

* Further cleanup

* Tests

* Increase test coverage

* Add test

* Increase test coverage

* Fix builds and update sdk

* Fix build

* Fix tests

* Reset changes to encrypt service

* Cleanup

* Add comment

* Cleanup

* Cleanup

* Rename model

* Cleanup

* Fix build

* Clean up

* Fix types

* Cleanup

* Cleanup

* Cleanup

* Add test

* Simplify request model

* Rename and add comments

* Fix tests

* Update responses to use less strict typing

* Fix response parsing for v1 users

* Update libs/common/src/key-management/keys/response/private-keys.response.ts

Co-authored-by: Maciej Zieniuk <167752252+mzieniukbw@users.noreply.github.com>

* Update libs/common/src/key-management/keys/response/private-keys.response.ts

Co-authored-by: Maciej Zieniuk <167752252+mzieniukbw@users.noreply.github.com>

* Fix build

* Fix build

* Fix build

* Undo change

* Fix attachments not encrypting for v2 users

---------

Co-authored-by: Thomas Avery <43214426+Thomas-Avery@users.noreply.github.com>
Co-authored-by: Maciej Zieniuk <167752252+mzieniukbw@users.noreply.github.com>
This commit is contained in:
Bernd Schoolmann
2025-10-10 23:04:47 +02:00
committed by GitHub
parent 89eb60135f
commit cc8bd71775
36 changed files with 1693 additions and 327 deletions

View File

@@ -100,6 +100,8 @@ import { InternalMasterPasswordServiceAbstraction } from "@bitwarden/common/key-
import { MasterPasswordService } from "@bitwarden/common/key-management/master-password/services/master-password.service"; import { MasterPasswordService } from "@bitwarden/common/key-management/master-password/services/master-password.service";
import { PinServiceAbstraction } from "@bitwarden/common/key-management/pin/pin.service.abstraction"; import { PinServiceAbstraction } from "@bitwarden/common/key-management/pin/pin.service.abstraction";
import { PinService } from "@bitwarden/common/key-management/pin/pin.service.implementation"; import { PinService } from "@bitwarden/common/key-management/pin/pin.service.implementation";
import { SecurityStateService } from "@bitwarden/common/key-management/security-state/abstractions/security-state.service";
import { DefaultSecurityStateService } from "@bitwarden/common/key-management/security-state/services/security-state.service";
import { DefaultProcessReloadService } from "@bitwarden/common/key-management/services/default-process-reload.service"; import { DefaultProcessReloadService } from "@bitwarden/common/key-management/services/default-process-reload.service";
import { import {
DefaultVaultTimeoutSettingsService, DefaultVaultTimeoutSettingsService,
@@ -452,6 +454,7 @@ export default class MainBackground {
taskService: TaskService; taskService: TaskService;
cipherEncryptionService: CipherEncryptionService; cipherEncryptionService: CipherEncryptionService;
private restrictedItemTypesService: RestrictedItemTypesService; private restrictedItemTypesService: RestrictedItemTypesService;
private securityStateService: SecurityStateService;
ipcContentScriptManagerService: IpcContentScriptManagerService; ipcContentScriptManagerService: IpcContentScriptManagerService;
ipcService: IpcService; ipcService: IpcService;
@@ -668,6 +671,8 @@ export default class MainBackground {
logoutCallback, logoutCallback,
); );
this.securityStateService = new DefaultSecurityStateService(this.stateProvider);
this.popupViewCacheBackgroundService = new PopupViewCacheBackgroundService( this.popupViewCacheBackgroundService = new PopupViewCacheBackgroundService(
messageListener, messageListener,
this.globalStateProvider, this.globalStateProvider,
@@ -830,6 +835,7 @@ export default class MainBackground {
this.accountService, this.accountService,
this.kdfConfigService, this.kdfConfigService,
this.keyService, this.keyService,
this.securityStateService,
this.apiService, this.apiService,
this.stateProvider, this.stateProvider,
this.configService, this.configService,
@@ -999,7 +1005,6 @@ export default class MainBackground {
this.avatarService = new AvatarService(this.apiService, this.stateProvider); this.avatarService = new AvatarService(this.apiService, this.stateProvider);
this.providerService = new ProviderService(this.stateProvider); this.providerService = new ProviderService(this.stateProvider);
this.syncService = new DefaultSyncService( this.syncService = new DefaultSyncService(
this.masterPasswordService, this.masterPasswordService,
this.accountService, this.accountService,
@@ -1025,6 +1030,7 @@ export default class MainBackground {
this.tokenService, this.tokenService,
this.authService, this.authService,
this.stateProvider, this.stateProvider,
this.securityStateService,
); );
this.syncServiceListener = new SyncServiceListener( this.syncServiceListener = new SyncServiceListener(

View File

@@ -73,6 +73,8 @@ import { InternalMasterPasswordServiceAbstraction } from "@bitwarden/common/key-
import { MasterPasswordService } from "@bitwarden/common/key-management/master-password/services/master-password.service"; import { MasterPasswordService } from "@bitwarden/common/key-management/master-password/services/master-password.service";
import { PinServiceAbstraction } from "@bitwarden/common/key-management/pin/pin.service.abstraction"; import { PinServiceAbstraction } from "@bitwarden/common/key-management/pin/pin.service.abstraction";
import { PinService } from "@bitwarden/common/key-management/pin/pin.service.implementation"; import { PinService } from "@bitwarden/common/key-management/pin/pin.service.implementation";
import { SecurityStateService } from "@bitwarden/common/key-management/security-state/abstractions/security-state.service";
import { DefaultSecurityStateService } from "@bitwarden/common/key-management/security-state/services/security-state.service";
import { import {
DefaultVaultTimeoutService, DefaultVaultTimeoutService,
DefaultVaultTimeoutSettingsService, DefaultVaultTimeoutSettingsService,
@@ -305,6 +307,7 @@ export class ServiceContainer {
cipherEncryptionService: CipherEncryptionService; cipherEncryptionService: CipherEncryptionService;
restrictedItemTypesService: RestrictedItemTypesService; restrictedItemTypesService: RestrictedItemTypesService;
cliRestrictedItemTypesService: CliRestrictedItemTypesService; cliRestrictedItemTypesService: CliRestrictedItemTypesService;
securityStateService: SecurityStateService;
cipherArchiveService: CipherArchiveService; cipherArchiveService: CipherArchiveService;
constructor() { constructor() {
@@ -406,6 +409,8 @@ export class ServiceContainer {
this.derivedStateProvider, this.derivedStateProvider,
); );
this.securityStateService = new DefaultSecurityStateService(this.stateProvider);
this.environmentService = new DefaultEnvironmentService( this.environmentService = new DefaultEnvironmentService(
this.stateProvider, this.stateProvider,
this.accountService, this.accountService,
@@ -612,6 +617,7 @@ export class ServiceContainer {
this.accountService, this.accountService,
this.kdfConfigService, this.kdfConfigService,
this.keyService, this.keyService,
this.securityStateService,
this.apiService, this.apiService,
this.stateProvider, this.stateProvider,
this.configService, this.configService,
@@ -818,6 +824,7 @@ export class ServiceContainer {
this.tokenService, this.tokenService,
this.authService, this.authService,
this.stateProvider, this.stateProvider,
this.securityStateService,
); );
this.totpService = new TotpService(this.sdkService); this.totpService = new TotpService(this.sdkService);

View File

@@ -56,7 +56,9 @@ export class ProfileComponent implements OnInit, OnDestroy {
this.profile = await this.apiService.getProfile(); this.profile = await this.apiService.getProfile();
const userId = await firstValueFrom(getUserId(this.accountService.activeAccount$)); const userId = await firstValueFrom(getUserId(this.accountService.activeAccount$));
this.fingerprintMaterial = userId; this.fingerprintMaterial = userId;
const publicKey = await firstValueFrom(this.keyService.userPublicKey$(userId)); const publicKey = (await firstValueFrom(
this.keyService.userPublicKey$(userId),
)) as UserPublicKey;
if (publicKey == null) { if (publicKey == null) {
this.logService.error( this.logService.error(
"[ProfileComponent] No public key available for the user: " + "[ProfileComponent] No public key available for the user: " +

View File

@@ -0,0 +1,19 @@
import { UnsignedPublicKey, WrappedPrivateKey } from "@bitwarden/common/key-management/types";
import { Utils } from "@bitwarden/common/platform/misc/utils";
import { SignedPublicKey } from "@bitwarden/sdk-internal";
export class PublicKeyEncryptionKeyPairRequestModel {
wrappedPrivateKey: WrappedPrivateKey;
publicKey: string;
signedPublicKey: SignedPublicKey | null;
constructor(
wrappedPrivateKey: WrappedPrivateKey,
publicKey: UnsignedPublicKey,
signedPublicKey: SignedPublicKey | null,
) {
this.wrappedPrivateKey = wrappedPrivateKey;
this.publicKey = Utils.fromBufferToB64(publicKey);
this.signedPublicKey = signedPublicKey;
}
}

View File

@@ -0,0 +1,18 @@
import { VerifyingKey, WrappedSigningKey } from "@bitwarden/common/key-management/types";
import { SignatureAlgorithm } from "@bitwarden/sdk-internal";
export class SignatureKeyPairRequestModel {
signatureAlgorithm: SignatureAlgorithm;
wrappedSigningKey: WrappedSigningKey;
verifyingKey: VerifyingKey;
constructor(
signingKey: WrappedSigningKey,
verifyingKey: VerifyingKey,
signingKeyAlgorithm: SignatureAlgorithm,
) {
this.signatureAlgorithm = signingKeyAlgorithm;
this.wrappedSigningKey = signingKey;
this.verifyingKey = verifyingKey;
}
}

View File

@@ -1,10 +1,70 @@
export class AccountKeysRequest { import { SecurityStateRequest } from "@bitwarden/common/key-management/security-state/request/security-state.request";
// Other keys encrypted by the userkey import { WrappedPrivateKey } from "@bitwarden/common/key-management/types";
userKeyEncryptedAccountPrivateKey: string; import { SdkLoadService } from "@bitwarden/common/platform/abstractions/sdk/sdk-load.service";
accountPublicKey: string; import { Utils } from "@bitwarden/common/platform/misc/utils";
import { PureCrypto } from "@bitwarden/sdk-internal";
constructor(userKeyEncryptedAccountPrivateKey: string, accountPublicKey: string) { import { PublicKeyEncryptionKeyPairRequestModel } from "../model/public-key-encryption-key-pair-request.model";
this.userKeyEncryptedAccountPrivateKey = userKeyEncryptedAccountPrivateKey; import { SignatureKeyPairRequestModel } from "../model/signature-key-pair-request-request.model";
this.accountPublicKey = accountPublicKey; import { V1UserCryptographicState } from "../types/v1-cryptographic-state";
import { V2UserCryptographicState } from "../types/v2-cryptographic-state";
// This request contains other account-owned keys that are encrypted with the user key.
export class AccountKeysRequest {
/**
* @deprecated
*/
userKeyEncryptedAccountPrivateKey: WrappedPrivateKey | null = null;
/**
* @deprecated
*/
accountPublicKey: string | null = null;
publicKeyEncryptionKeyPair: PublicKeyEncryptionKeyPairRequestModel | null = null;
signatureKeyPair: SignatureKeyPairRequestModel | null = null;
securityState: SecurityStateRequest | null = null;
constructor() {}
static fromV1CryptographicState(state: V1UserCryptographicState): AccountKeysRequest {
const request = new AccountKeysRequest();
request.userKeyEncryptedAccountPrivateKey = state.publicKeyEncryptionKeyPair.wrappedPrivateKey;
request.accountPublicKey = Utils.fromBufferToB64(state.publicKeyEncryptionKeyPair.publicKey);
request.publicKeyEncryptionKeyPair = new PublicKeyEncryptionKeyPairRequestModel(
state.publicKeyEncryptionKeyPair.wrappedPrivateKey,
state.publicKeyEncryptionKeyPair.publicKey,
null,
);
return request;
}
static async fromV2CryptographicState(
state: V2UserCryptographicState,
): Promise<AccountKeysRequest> {
// Ensure the SDK is loaded, since it is used to derive the signature algorithm.
await SdkLoadService.Ready;
const request = new AccountKeysRequest();
request.userKeyEncryptedAccountPrivateKey = state.publicKeyEncryptionKeyPair.wrappedPrivateKey!;
request.accountPublicKey = Utils.fromBufferToB64(state.publicKeyEncryptionKeyPair.publicKey);
request.publicKeyEncryptionKeyPair = new PublicKeyEncryptionKeyPairRequestModel(
state.publicKeyEncryptionKeyPair.wrappedPrivateKey,
state.publicKeyEncryptionKeyPair.publicKey,
state.publicKeyEncryptionKeyPair.signedPublicKey,
);
request.signatureKeyPair = new SignatureKeyPairRequestModel(
state.signatureKeyPair.wrappedSigningKey,
state.signatureKeyPair.verifyingKey,
PureCrypto.key_algorithm_for_verifying_key(
Utils.fromB64ToArray(state.signatureKeyPair.verifyingKey),
),
);
request.securityState = new SecurityStateRequest(
state.securityState.securityState,
state.securityState.securityStateVersion,
);
return request;
} }
} }

View File

@@ -0,0 +1,10 @@
import { UnsignedPublicKey, WrappedPrivateKey } from "@bitwarden/common/key-management/types";
import { UserKey } from "@bitwarden/common/types/key";
export type V1UserCryptographicState = {
userKey: UserKey;
publicKeyEncryptionKeyPair: {
wrappedPrivateKey: WrappedPrivateKey;
publicKey: UnsignedPublicKey;
};
};

View File

@@ -0,0 +1,49 @@
import {
SignedSecurityState,
UnsignedPublicKey,
VerifyingKey,
WrappedPrivateKey,
WrappedSigningKey,
} from "@bitwarden/common/key-management/types";
import { Utils } from "@bitwarden/common/platform/misc/utils";
import { SymmetricCryptoKey } from "@bitwarden/common/platform/models/domain/symmetric-crypto-key";
import { UserKey } from "@bitwarden/common/types/key";
import { SignedPublicKey, UserCryptoV2KeysResponse } from "@bitwarden/sdk-internal";
export type V2UserCryptographicState = {
userKey: UserKey;
publicKeyEncryptionKeyPair: {
wrappedPrivateKey: WrappedPrivateKey;
publicKey: UnsignedPublicKey;
signedPublicKey: SignedPublicKey;
};
signatureKeyPair: {
wrappedSigningKey: WrappedSigningKey;
verifyingKey: VerifyingKey;
};
securityState: {
securityState: SignedSecurityState;
securityStateVersion: number;
};
};
export function fromSdkV2KeysToV2UserCryptographicState(
response: UserCryptoV2KeysResponse,
): V2UserCryptographicState {
return {
userKey: SymmetricCryptoKey.fromString(response.userKey) as UserKey,
publicKeyEncryptionKeyPair: {
wrappedPrivateKey: response.privateKey as WrappedPrivateKey,
publicKey: Utils.fromB64ToArray(response.publicKey) as UnsignedPublicKey,
signedPublicKey: response.signedPublicKey,
},
signatureKeyPair: {
wrappedSigningKey: response.signingKey as WrappedSigningKey,
verifyingKey: response.verifyingKey as VerifyingKey,
},
securityState: {
securityState: response.securityState as SignedSecurityState,
securityStateVersion: response.securityVersion,
},
};
}

View File

@@ -4,6 +4,7 @@ import { BehaviorSubject } from "rxjs";
import { OrganizationUserResetPasswordWithIdRequest } from "@bitwarden/admin-console/common"; import { OrganizationUserResetPasswordWithIdRequest } from "@bitwarden/admin-console/common";
import { Account } from "@bitwarden/common/auth/abstractions/account.service"; import { Account } from "@bitwarden/common/auth/abstractions/account.service";
import { WebauthnRotateCredentialRequest } from "@bitwarden/common/auth/models/request/webauthn-rotate-credential.request"; import { WebauthnRotateCredentialRequest } from "@bitwarden/common/auth/models/request/webauthn-rotate-credential.request";
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
import { CryptoFunctionService } from "@bitwarden/common/key-management/crypto/abstractions/crypto-function.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 { EncryptService } from "@bitwarden/common/key-management/crypto/abstractions/encrypt.service";
import { import {
@@ -11,10 +12,22 @@ import {
EncString, EncString,
} from "@bitwarden/common/key-management/crypto/models/enc-string"; } from "@bitwarden/common/key-management/crypto/models/enc-string";
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 {
SignedPublicKey,
SignedSecurityState,
UnsignedPublicKey,
VerifyingKey,
WrappedPrivateKey,
WrappedSigningKey,
} from "@bitwarden/common/key-management/types";
import { VaultTimeoutService } from "@bitwarden/common/key-management/vault-timeout"; import { VaultTimeoutService } from "@bitwarden/common/key-management/vault-timeout";
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
import { SdkClientFactory } from "@bitwarden/common/platform/abstractions/sdk/sdk-client-factory";
import { SdkLoadService } from "@bitwarden/common/platform/abstractions/sdk/sdk-load.service";
import { HashPurpose } from "@bitwarden/common/platform/enums";
import { Utils } from "@bitwarden/common/platform/misc/utils"; import { Utils } from "@bitwarden/common/platform/misc/utils";
import { SymmetricCryptoKey } from "@bitwarden/common/platform/models/domain/symmetric-crypto-key"; import { SymmetricCryptoKey } from "@bitwarden/common/platform/models/domain/symmetric-crypto-key";
import { SendWithIdRequest } from "@bitwarden/common/tools/send/models/request/send-with-id.request"; import { SendWithIdRequest } from "@bitwarden/common/tools/send/models/request/send-with-id.request";
@@ -33,13 +46,14 @@ import {
PBKDF2KdfConfig, PBKDF2KdfConfig,
KdfConfigService, KdfConfigService,
KdfConfig, KdfConfig,
KdfType,
} from "@bitwarden/key-management"; } from "@bitwarden/key-management";
import { import {
AccountRecoveryTrustComponent, AccountRecoveryTrustComponent,
EmergencyAccessTrustComponent, EmergencyAccessTrustComponent,
KeyRotationTrustInfoComponent, KeyRotationTrustInfoComponent,
} from "@bitwarden/key-management-ui"; } from "@bitwarden/key-management-ui";
import { PureCrypto } from "@bitwarden/sdk-internal"; import { BitwardenClient, PureCrypto } from "@bitwarden/sdk-internal";
import { OrganizationUserResetPasswordService } from "../../admin-console/organizations/members/services/organization-user-reset-password/organization-user-reset-password.service"; import { OrganizationUserResetPasswordService } from "../../admin-console/organizations/members/services/organization-user-reset-password/organization-user-reset-password.service";
import { WebauthnLoginAdminService } from "../../auth"; import { WebauthnLoginAdminService } from "../../auth";
@@ -48,11 +62,18 @@ import { EmergencyAccessStatusType } from "../../auth/emergency-access/enums/eme
import { EmergencyAccessType } from "../../auth/emergency-access/enums/emergency-access-type"; import { EmergencyAccessType } from "../../auth/emergency-access/enums/emergency-access-type";
import { EmergencyAccessWithIdRequest } from "../../auth/emergency-access/request/emergency-access-update.request"; import { EmergencyAccessWithIdRequest } from "../../auth/emergency-access/request/emergency-access-update.request";
import { AccountKeysRequest } from "./request/account-keys.request";
import { MasterPasswordUnlockDataRequest } from "./request/master-password-unlock-data.request"; import { MasterPasswordUnlockDataRequest } from "./request/master-password-unlock-data.request";
import { UnlockDataRequest } from "./request/unlock-data.request"; import { UnlockDataRequest } from "./request/unlock-data.request";
import { UserDataRequest } from "./request/userdata.request"; import { UserDataRequest } from "./request/userdata.request";
import { V1UserCryptographicState } from "./types/v1-cryptographic-state";
import { V2UserCryptographicState } from "./types/v2-cryptographic-state";
import { UserKeyRotationApiService } from "./user-key-rotation-api.service"; import { UserKeyRotationApiService } from "./user-key-rotation-api.service";
import { UserKeyRotationService } from "./user-key-rotation.service"; import {
UserKeyRotationService,
V1CryptographicStateParameters,
V2CryptographicStateParameters,
} from "./user-key-rotation.service";
const initialPromptedOpenTrue = jest.fn(); const initialPromptedOpenTrue = jest.fn();
initialPromptedOpenTrue.mockReturnValue({ closed: new BehaviorSubject(true) }); initialPromptedOpenTrue.mockReturnValue({ closed: new BehaviorSubject(true) });
@@ -120,6 +141,21 @@ function createMockWebauthn(id: string): any {
} as WebauthnRotateCredentialRequest; } as WebauthnRotateCredentialRequest;
} }
const TEST_VECTOR_USER_KEY_V1 = new SymmetricCryptoKey(new Uint8Array(64)) as UserKey;
const TEST_VECTOR_PRIVATE_KEY_V1 =
"2.AAAw2vTUePO+CCyokcIfVw==|DTBNlJ5yVsV2Bsk3UU3H6Q==|YvFBff5gxWqM+UsFB6BKimKxhC32AtjF3IStpU1Ijwg=" as WrappedPrivateKey;
const TEST_VECTOR_PUBLIC_KEY_V1 = Utils.fromBufferToB64(new Uint8Array(400));
const TEST_VECTOR_PRIVATE_KEY_V1_ROTATED =
"2.AAAw2vTUePO+CCyokcIfVw==|DTBNlJ5yVsV2Bsk3UU3H6Q==|AAAAff5gxWqM+UsFB6BKimKxhC32AtjF3IStpU1Ijwg=" as WrappedPrivateKey;
const TEST_VECTOR_USER_KEY_V2 = new SymmetricCryptoKey(new Uint8Array(70)) as UserKey;
const TEST_VECTOR_PRIVATE_KEY_V2 = "7.AAAw2vTUePO+CCyokcIfVw==" as WrappedPrivateKey;
const TEST_VECTOR_SIGNING_KEY_V2 = "7.AAAw2vTUePO+CCyokcIfVw==" as WrappedSigningKey;
const TEST_VECTOR_VERIFYING_KEY_V2 = "AAAw2vTUePO+CCyokcIfVw==" as VerifyingKey;
const TEST_VECTOR_SECURITY_STATE_V2 = "AAAw2vTUePO+CCyokcIfVw==" as SignedSecurityState;
const TEST_VECTOR_PUBLIC_KEY_V2 = Utils.fromBufferToB64(new Uint8Array(400));
const TEST_VECTOR_SIGNED_PUBLIC_KEY_V2 = "AAAw2vTUePO+CCyokcIfVw==" as SignedPublicKey;
class TestUserKeyRotationService extends UserKeyRotationService { class TestUserKeyRotationService extends UserKeyRotationService {
override rotateUserKeyMasterPasswordAndEncryptedData( override rotateUserKeyMasterPasswordAndEncryptedData(
currentMasterPassword: string, currentMasterPassword: string,
@@ -138,22 +174,17 @@ class TestUserKeyRotationService extends UserKeyRotationService {
return super.ensureIsAllowedToRotateUserKey(); return super.ensureIsAllowedToRotateUserKey();
} }
override getNewAccountKeysV1( override getNewAccountKeysV1(
currentUserKey: UserKey, cryptographicStateParameters: V1CryptographicStateParameters,
currentUserKeyWrappedPrivateKey: EncString, ): Promise<V1UserCryptographicState> {
): Promise<{ return super.getNewAccountKeysV1(cryptographicStateParameters);
userKey: UserKey;
asymmetricEncryptionKeys: { wrappedPrivateKey: EncString; publicKey: string };
}> {
return super.getNewAccountKeysV1(currentUserKey, currentUserKeyWrappedPrivateKey);
} }
override getNewAccountKeysV2( override getNewAccountKeysV2(
currentUserKey: UserKey, userId: UserId,
currentUserKeyWrappedPrivateKey: EncString, kdfConfig: KdfConfig,
): Promise<{ email: string,
userKey: UserKey; cryptographicStateParameters: V1CryptographicStateParameters | V2CryptographicStateParameters,
asymmetricEncryptionKeys: { wrappedPrivateKey: EncString; publicKey: string }; ): Promise<V2UserCryptographicState> {
}> { return super.getNewAccountKeysV2(userId, kdfConfig, email, cryptographicStateParameters);
return super.getNewAccountKeysV2(currentUserKey, currentUserKeyWrappedPrivateKey);
} }
override createMasterPasswordUnlockDataRequest( override createMasterPasswordUnlockDataRequest(
userKey: UserKey, userKey: UserKey,
@@ -176,8 +207,8 @@ class TestUserKeyRotationService extends UserKeyRotationService {
masterKeyKdfConfig: KdfConfig; masterKeyKdfConfig: KdfConfig;
masterPasswordHint: string; masterPasswordHint: string;
}, },
trustedEmergencyAccessGranteesPublicKeys: Uint8Array[], trustedEmergencyAccessGranteesPublicKeys: UnsignedPublicKey[],
trustedOrganizationPublicKeys: Uint8Array[], trustedOrganizationPublicKeys: UnsignedPublicKey[],
): Promise<UnlockDataRequest> { ): Promise<UnlockDataRequest> {
return super.getAccountUnlockDataRequest( return super.getAccountUnlockDataRequest(
userId, userId,
@@ -190,8 +221,8 @@ class TestUserKeyRotationService extends UserKeyRotationService {
} }
override verifyTrust(user: Account): Promise<{ override verifyTrust(user: Account): Promise<{
wasTrustDenied: boolean; wasTrustDenied: boolean;
trustedOrganizationPublicKeys: Uint8Array[]; trustedOrganizationPublicKeys: UnsignedPublicKey[];
trustedEmergencyAccessUserPublicKeys: Uint8Array[]; trustedEmergencyAccessUserPublicKeys: UnsignedPublicKey[];
}> { }> {
return super.verifyTrust(user); return super.verifyTrust(user);
} }
@@ -202,14 +233,6 @@ class TestUserKeyRotationService extends UserKeyRotationService {
): Promise<UserDataRequest> { ): Promise<UserDataRequest> {
return super.getAccountDataRequest(originalUserKey, newUnencryptedUserKey, user); return super.getAccountDataRequest(originalUserKey, newUnencryptedUserKey, user);
} }
override makeNewUserKeyV1(oldUserKey: UserKey): Promise<UserKey> {
return super.makeNewUserKeyV1(oldUserKey);
}
override makeNewUserKeyV2(
oldUserKey: UserKey,
): Promise<{ isUpgrading: boolean; newUserKey: UserKey }> {
return super.makeNewUserKeyV2(oldUserKey);
}
override isV1User(userKey: UserKey): boolean { override isV1User(userKey: UserKey): boolean {
return super.isV1User(userKey); return super.isV1User(userKey);
} }
@@ -227,6 +250,13 @@ class TestUserKeyRotationService extends UserKeyRotationService {
masterKeySalt, masterKeySalt,
); );
} }
override getCryptographicStateForUser(user: Account): Promise<{
masterKeyKdfConfig: KdfConfig;
masterKeySalt: string;
cryptographicStateParameters: V1CryptographicStateParameters | V2CryptographicStateParameters;
}> {
return super.getCryptographicStateForUser(user);
}
} }
describe("KeyRotationService", () => { describe("KeyRotationService", () => {
@@ -251,6 +281,8 @@ describe("KeyRotationService", () => {
let mockI18nService: MockProxy<I18nService>; let mockI18nService: MockProxy<I18nService>;
let mockCryptoFunctionService: MockProxy<CryptoFunctionService>; let mockCryptoFunctionService: MockProxy<CryptoFunctionService>;
let mockKdfConfigService: MockProxy<KdfConfigService>; let mockKdfConfigService: MockProxy<KdfConfigService>;
let mockSdkClientFactory: MockProxy<SdkClientFactory>;
let mockSecurityStateService: MockProxy<SecurityStateService>;
const mockUser = { const mockUser = {
id: "mockUserId" as UserId, id: "mockUserId" as UserId,
@@ -261,6 +293,9 @@ describe("KeyRotationService", () => {
const mockTrustedPublicKeys = [Utils.fromUtf8ToArray("test-public-key")]; const mockTrustedPublicKeys = [Utils.fromUtf8ToArray("test-public-key")];
const mockMakeKeysForUserCryptoV2 = jest.fn();
const mockGetV2RotatedAccountKeys = jest.fn();
beforeAll(() => { beforeAll(() => {
mockApiService = mock<UserKeyRotationApiService>(); mockApiService = mock<UserKeyRotationApiService>();
mockCipherService = mock<CipherService>(); mockCipherService = mock<CipherService>();
@@ -271,7 +306,7 @@ describe("KeyRotationService", () => {
mockTrustedPublicKeys.map((key) => { mockTrustedPublicKeys.map((key) => {
return { return {
publicKey: key, publicKey: key,
id: "mockId", id: "00000000-0000-0000-0000-000000000000" as UserId,
granteeId: "mockGranteeId", granteeId: "mockGranteeId",
name: "mockName", name: "mockName",
email: "mockEmail", email: "mockEmail",
@@ -306,6 +341,17 @@ describe("KeyRotationService", () => {
mockDialogService = mock<DialogService>(); mockDialogService = mock<DialogService>();
mockCryptoFunctionService = mock<CryptoFunctionService>(); mockCryptoFunctionService = mock<CryptoFunctionService>();
mockKdfConfigService = mock<KdfConfigService>(); mockKdfConfigService = mock<KdfConfigService>();
mockSdkClientFactory = mock<SdkClientFactory>();
mockSdkClientFactory.createSdkClient.mockResolvedValue({
crypto: () => {
return {
initialize_user_crypto: jest.fn(),
make_keys_for_user_crypto_v2: mockMakeKeysForUserCryptoV2,
get_v2_rotated_account_keys: mockGetV2RotatedAccountKeys,
} as any;
},
} as BitwardenClient);
mockSecurityStateService = mock<SecurityStateService>();
keyRotationService = new TestUserKeyRotationService( keyRotationService = new TestUserKeyRotationService(
mockApiService, mockApiService,
@@ -327,6 +373,8 @@ describe("KeyRotationService", () => {
mockConfigService, mockConfigService,
mockCryptoFunctionService, mockCryptoFunctionService,
mockKdfConfigService, mockKdfConfigService,
mockSdkClientFactory,
mockSecurityStateService,
); );
}); });
@@ -334,13 +382,16 @@ describe("KeyRotationService", () => {
jest.clearAllMocks(); jest.clearAllMocks();
jest.mock("@bitwarden/key-management-ui"); jest.mock("@bitwarden/key-management-ui");
jest.spyOn(PureCrypto, "make_user_key_aes256_cbc_hmac").mockReturnValue(new Uint8Array(64)); jest.spyOn(PureCrypto, "make_user_key_aes256_cbc_hmac").mockReturnValue(new Uint8Array(64));
jest.spyOn(PureCrypto, "make_user_key_xchacha20_poly1305").mockReturnValue(new Uint8Array(70));
jest jest
.spyOn(PureCrypto, "encrypt_user_key_with_master_password") .spyOn(PureCrypto, "encrypt_user_key_with_master_password")
.mockReturnValue("mockNewUserKey"); .mockReturnValue("mockNewUserKey");
Object.defineProperty(SdkLoadService, "Ready", {
value: Promise.resolve(),
configurable: true,
});
}); });
describe("rotateUserKeyAndEncryptedData", () => { describe("rotateUserKeyMasterPasswordAndEncryptedData", () => {
let privateKey: BehaviorSubject<UserPrivateKey | null>; let privateKey: BehaviorSubject<UserPrivateKey | null>;
let keyPair: BehaviorSubject<{ privateKey: UserPrivateKey; publicKey: UserPublicKey }>; let keyPair: BehaviorSubject<{ privateKey: UserPrivateKey; publicKey: UserPublicKey }>;
@@ -438,6 +489,64 @@ describe("KeyRotationService", () => {
expect(arg.accountUnlockData.passkeyUnlockData.length).toBe(2); expect(arg.accountUnlockData.passkeyUnlockData.length).toBe(2);
}); });
it("passes the EnrollAeadOnKeyRotation feature flag to getRotatedAccountKeysFlagged", async () => {
KeyRotationTrustInfoComponent.open = initialPromptedOpenTrue;
AccountRecoveryTrustComponent.open = accountRecoveryTrustOpenTrusted;
EmergencyAccessTrustComponent.open = emergencyAccessTrustOpenTrusted;
mockKdfConfigService.getKdfConfig$.mockReturnValue(
new BehaviorSubject(new PBKDF2KdfConfig(100000)),
);
mockKeyService.userKey$.mockReturnValue(
new BehaviorSubject(new SymmetricCryptoKey(new Uint8Array(64)) as UserKey),
);
mockKeyService.userEncryptedPrivateKey$.mockReturnValue(
new BehaviorSubject(TEST_VECTOR_PRIVATE_KEY_V1 as string as EncryptedString),
);
mockKeyService.userSigningKey$.mockReturnValue(new BehaviorSubject(null));
mockSecurityStateService.accountSecurityState$.mockReturnValue(new BehaviorSubject(null));
mockConfigService.getFeatureFlag.mockResolvedValue(true);
const spy = jest.spyOn(keyRotationService, "getRotatedAccountKeysFlagged").mockResolvedValue({
userKey: TEST_VECTOR_USER_KEY_V2,
accountKeysRequest: {
userKeyEncryptedAccountPrivateKey: TEST_VECTOR_PRIVATE_KEY_V2,
accountPublicKey: TEST_VECTOR_PUBLIC_KEY_V2,
publicKeyEncryptionKeyPair: {
wrappedPrivateKey: TEST_VECTOR_PRIVATE_KEY_V2,
publicKey: TEST_VECTOR_PUBLIC_KEY_V2,
signedPublicKey: TEST_VECTOR_SIGNED_PUBLIC_KEY_V2,
},
signatureKeyPair: {
wrappedSigningKey: TEST_VECTOR_SIGNING_KEY_V2,
verifyingKey: TEST_VECTOR_VERIFYING_KEY_V2,
signatureAlgorithm: "ed25519",
},
securityState: {
securityState: TEST_VECTOR_SECURITY_STATE_V2,
securityVersion: 2,
},
},
});
await keyRotationService.rotateUserKeyMasterPasswordAndEncryptedData(
"mockMasterPassword",
"mockMasterPassword1",
mockUser,
"masterPasswordHint",
);
expect(mockConfigService.getFeatureFlag).toHaveBeenCalledWith(
FeatureFlag.EnrollAeadOnKeyRotation,
);
expect(spy).toHaveBeenCalledWith(
mockUser.id,
expect.any(PBKDF2KdfConfig),
mockUser.email,
expect.objectContaining({ version: 1 }),
true,
);
});
it("throws if kdf config is null", async () => { it("throws if kdf config is null", async () => {
KeyRotationTrustInfoComponent.open = initialPromptedOpenTrue; KeyRotationTrustInfoComponent.open = initialPromptedOpenTrue;
AccountRecoveryTrustComponent.open = accountRecoveryTrustOpenTrusted; AccountRecoveryTrustComponent.open = accountRecoveryTrustOpenTrusted;
@@ -511,17 +620,17 @@ describe("KeyRotationService", () => {
}); });
describe("getNewAccountKeysV1", () => { describe("getNewAccountKeysV1", () => {
const currentUserKey = new SymmetricCryptoKey(new Uint8Array(64)) as UserKey; const currentUserKey = TEST_VECTOR_USER_KEY_V1;
const mockEncryptedPrivateKey = new EncString( const mockEncryptedPrivateKey = TEST_VECTOR_PRIVATE_KEY_V1 as WrappedPrivateKey;
"2.eh465OrUcluL9UpnCOUTAg==|2HXNXwrLwAjUfZ/U75c92rZEltt1eHxjMkp/ADAmx346oT1+GaQvaL1QIV/9Om0T72m8AnlO92iUfWdhbA/ifHZ+lhFoUVeyw1M88CMzktbVcq42rFoK7SGHSAGdTL3ccUWKI8yCCQJhpt2X6a/5+T7ey5k2CqvylKyOtkiCnVeLmYqETn5BM9Rl3tEgJW1yDLuSJ+L+Qh9xnk/Z3zJUV5HAs+YwjKwuSNrd00SXjDyx8rBEstD9MKI+lrk7to/q90vqKqCucAj/dzUpVtHe88al2AAlBVwQ13HUPdNFOyti6niUgCAWx+DzRqlhkFvl/z/rtxtQsyqq/3Eh/EL54ylxKzAya0ev9EaIOm/dD1aBmI58p4Bs0eMOCIKJjtw+Cmdql+RhCtKtumgFShqyXv+LfD/FgUsdTVNExk3YNhgwPR4jOaMa/j9LCrBMCLKxdAhQyBe7T3qoX1fBBirvY6t77ifMu1YEQ6DfmFphVSwDH5C9xGeTSh5IELSf0tGVtlWUe9RffDDzccD0L1lR8U+dqzoSTYCuXvhEhQptdIW6fpH/47u0M5MiI97/d35A7Et2I1gjHp7WF3qsY20ellBueu7ZL5P1BmqPXl58yaBBXJaCutYHDfIucspqdZmfBGEbdRT4wmuZRON0J8zLmUejM0VR/2MOmpfyYQXnJhTfrvnZ1bOg1aMhUxJ2vhDNPXUFm5b+vwsho4GEvcLAKq9WwbvOJ/sK7sEVfTfEO2IG+0X6wkWm7RpR6Wq9FGKSrv2PSjMAYnb+z3ETeWiaaiD+tVFxa2AaqsbOuX092/86GySpHES7cFWhQ/YMOgj6egUi8mEC0CqMXYsx0TTJDsn16oP+XB3a2WoRqzE0YBozp2aMXxhVf/jMZ03BmEmRQu5B+Sq1gMEZwtIfJ+srkZLMYlLjvVw92FRoFy+N6ytPiyf6RMHMUnJ3vEZSBogaElYoQAtFJ5kK811CUzb78zEHH8xWtPrCZn9zZfvf/zaWxo7fpV8VwAwUeHXHcQMraZum5QeO+5tLRUYrLm85JNelGfmUA3BjfNyFbfb32PhkWWd0CbDaPME48uIriVK32pNEtvtR/+I/f3YgA/jP9kSlDvbzG/OAg/AFBIpNwKUzsu4+va8mI+O5FDufw5D74WwdGJ9DeyEb2CHtWMR1VwtFKL0ZZsqltNf8EkBeJ5RtTNtAMM8ie4dDZaKC96ymQHKrdB4hjkAr0F1XFsU4XdOa9Nbkdcm/7KoNc6bE6oJtG9lqE8h+1CysfcbfJ7am+hvDFzT0IPmp3GDSMAk+e6xySgFQw0C/SZ7LQsxPa1s6hc+BOtTn0oClZnU7Mowxv+z+xURJj4Yp3Cy6tAoia1jEQSs6lSMNKPf9bi3xFKtPl4143hwhpvTAzJUcski9OVGd7Du+VyxwIrvLqp5Ct/oNrESVJpf1EDCs9xT1EW+PiSkRmHXoZ1t5MOLFEiMAZL2+bNe3A2661oJeMtps8zrfCVc251OUE1WvqWePlTOs5TDVqdwDH88J6rHLsbaf33Mxh5DP8gMfZQxE44Nsp6H0/Szfkss5UmFwBEpHjl1GJMWDnB3u2d+l1CSkLoB6C+diAUlY6wL/VwJBeMPHZTf6amQIS2B/lo/CnvV/E3k=|uuoY4b7xwMYBNIZi85KBsaHmNqtJl5FrKxZI9ugeNwc=", const mockNewEncryptedPrivateKey = TEST_VECTOR_PRIVATE_KEY_V1_ROTATED as WrappedPrivateKey;
);
const mockNewEncryptedPrivateKey = new EncString(
"2.ab465OrUcluL9UpnCOUTAg==|4HXNXwrLwAjUfZ/U75c92rZEltt1eHxjMkp/ADAmx346oT1+GaQvaL1QIV/9Om0T72m8AnlO92iUfWdhbA/ifHZ+lhFoUVeyw1M88CMzktbVcq42rFoK7SGHSAGdTL3ccUWKI8yCCQJhpt2X6a/5+T7ey5k2CqvylKyOtkiCnVeLmYqETn5BM9Rl3tEgJW1yDLuSJ+L+Qh9xnk/Z3zJUV5HAs+YwjKwuSNrd00SXjDyx8rBEstD9MKI+lrk7to/q90vqKqCucAj/dzUpVtHe88al2AAlBVwQ13HUPdNFOyti6niUgCAWx+DzRqlhkFvl/z/rtxtQsyqq/3Eh/EL54ylxKzAya0ev9EaIOm/dD1aBmI58p4Bs0eMOCIKJjtw+Cmdql+RhCtKtumgFShqyXv+LfD/FgUsdTVNExk3YNhgwPR4jOaMa/j9LCrBMCLKxdAhQyBe7T3qoX1fBBirvY6t77ifMu1YEQ6DfmFphVSwDH5C9xGeTSh5IELSf0tGVtlWUe9RffDDzccD0L1lR8U+dqzoSTYCuXvhEhQptdIW6fpH/47u0M5MiI97/d35A7Et2I1gjHp7WF3qsY20ellBueu7ZL5P1BmqPXl58yaBBXJaCutYHDfIucspqdZmfBGEbdRT4wmuZRON0J8zLmUejM0VR/2MOmpfyYQXnJhTfrvnZ1bOg1aMhUxJ2vhDNPXUFm5b+vwsho4GEvcLAKq9WwbvOJ/sK7sEVfTfEO2IG+0X6wkWm7RpR6Wq9FGKSrv2PSjMAYnb+z3ETeWiaaiD+tVFxa2AaqsbOuX092/86GySpHES7cFWhQ/YMOgj6egUi8mEC0CqMXYsx0TTJDsn16oP+XB3a2WoRqzE0YBozp2aMXxhVf/jMZ03BmEmRQu5B+Sq1gMEZwtIfJ+srkZLMYlLjvVw92FRoFy+N6ytPiyf6RMHMUnJ3vEZSBogaElYoQAtFJ5kK811CUzb78zEHH8xWtPrCZn9zZfvf/zaWxo7fpV8VwAwUeHXHcQMraZum5QeO+5tLRUYrLm85JNelGfmUA3BjfNyFbfb32PhkWWd0CbDaPME48uIriVK32pNEtvtR/+I/f3YgA/jP9kSlDvbzG/OAg/AFBIpNwKUzsu4+va8mI+O5FDufw5D74WwdGJ9DeyEb2CHtWMR1VwtFKL0ZZsqltNf8EkBeJ5RtTNtAMM8ie4dDZaKC96ymQHKrdB4hjkAr0F1XFsU4XdOa9Nbkdcm/7KoNc6bE6oJtG9lqE8h+1CysfcbfJ7am+hvDFzT0IPmp3GDSMAk+e6xySgFQw0C/SZ7LQsxPa1s6hc+BOtTn0oClZnU7Mowxv+z+xURJj4Yp3Cy6tAoia1jEQSs6lSMNKPf9bi3xFKtPl4143hwhpvTAzJUcski9OVGd7Du+VyxwIrvLqp5Ct/oNrESVJpf1EDCs9xT1EW+PiSkRmHXoZ1t5MOLFEiMAZL2+bNe3A2661oJeMtps8zrfCVc251OUE1WvqWePlTOs5TDVqdwDH88J6rHLsbaf33Mxh5DP8gMfZQxE44Nsp6H0/Szfkss5UmFwBEpHjl1GJMWDnB3u2d+l1CSkLoB6C+diAUlY6wL/VwJBeMPHZTf6amQIS2B/lo/CnvV/E3k=|uuoY4b7xwMYBNIZi85KBsaHmNqtJl5FrKxZI9ugeNwc=",
);
beforeAll(() => { beforeAll(() => {
mockEncryptService.unwrapDecapsulationKey.mockResolvedValue(new Uint8Array(200)); mockEncryptService.unwrapDecapsulationKey.mockResolvedValue(new Uint8Array(200));
mockEncryptService.wrapDecapsulationKey.mockResolvedValue(mockNewEncryptedPrivateKey); mockEncryptService.wrapDecapsulationKey.mockResolvedValue(
mockCryptoFunctionService.rsaExtractPublicKey.mockResolvedValue(new Uint8Array(400)); new EncString(mockNewEncryptedPrivateKey),
);
mockCryptoFunctionService.rsaExtractPublicKey.mockResolvedValue(
new Uint8Array(400) as UnsignedPublicKey,
);
}); });
afterAll(() => { afterAll(() => {
@@ -529,28 +638,110 @@ describe("KeyRotationService", () => {
}); });
it("returns new account keys", async () => { it("returns new account keys", async () => {
const result = await keyRotationService.getNewAccountKeysV1( const result = await keyRotationService.getNewAccountKeysV1({
currentUserKey, version: 1,
mockEncryptedPrivateKey, userKey: currentUserKey,
); publicKeyEncryptionKeyPair: {
wrappedPrivateKey: mockEncryptedPrivateKey,
publicKey: Utils.fromB64ToArray(TEST_VECTOR_PUBLIC_KEY_V1) as UnsignedPublicKey,
},
});
expect(result).toEqual({ expect(result).toEqual({
userKey: expect.any(SymmetricCryptoKey), userKey: expect.any(SymmetricCryptoKey),
asymmetricEncryptionKeys: { publicKeyEncryptionKeyPair: {
wrappedPrivateKey: mockNewEncryptedPrivateKey, wrappedPrivateKey: mockNewEncryptedPrivateKey,
publicKey: Utils.fromBufferToB64(new Uint8Array(400)), publicKey: new Uint8Array(400) as UserPublicKey,
}, },
}); });
}); });
}); });
describe("getNewAccountKeysV2", () => { describe("getNewAccountKeysV2", () => {
it("throws not supported", async () => { it("rotates a v2 user", async () => {
await expect( mockGetV2RotatedAccountKeys.mockReturnValue({
keyRotationService.getNewAccountKeysV2( userKey: TEST_VECTOR_USER_KEY_V2.toBase64(),
new SymmetricCryptoKey(new Uint8Array(64)) as UserKey, privateKey: TEST_VECTOR_PRIVATE_KEY_V2,
null, publicKey: TEST_VECTOR_PUBLIC_KEY_V2,
), signedPublicKey: TEST_VECTOR_SIGNED_PUBLIC_KEY_V2,
).rejects.toThrow("User encryption v2 upgrade is not supported yet"); signingKey: TEST_VECTOR_SIGNING_KEY_V2,
verifyingKey: TEST_VECTOR_VERIFYING_KEY_V2,
securityState: TEST_VECTOR_SECURITY_STATE_V2,
securityVersion: 2,
});
const result = await keyRotationService.getNewAccountKeysV2(
"00000000-0000-0000-0000-000000000000" as UserId,
new PBKDF2KdfConfig(600_000),
"mockuseremail",
{
version: 2 as const,
userKey: TEST_VECTOR_USER_KEY_V2,
publicKeyEncryptionKeyPair: {
wrappedPrivateKey: TEST_VECTOR_PRIVATE_KEY_V2,
publicKey: Utils.fromB64ToArray(TEST_VECTOR_PUBLIC_KEY_V2) as UnsignedPublicKey,
},
signingKey: TEST_VECTOR_SIGNING_KEY_V2 as WrappedSigningKey,
securityState: TEST_VECTOR_SECURITY_STATE_V2 as SignedSecurityState,
},
);
expect(mockGetV2RotatedAccountKeys).toHaveBeenCalled();
expect(result).toEqual({
userKey: TEST_VECTOR_USER_KEY_V2,
publicKeyEncryptionKeyPair: {
wrappedPrivateKey: TEST_VECTOR_PRIVATE_KEY_V2,
publicKey: Utils.fromB64ToArray(TEST_VECTOR_PUBLIC_KEY_V2) as UnsignedPublicKey,
signedPublicKey: TEST_VECTOR_SIGNED_PUBLIC_KEY_V2,
},
signatureKeyPair: {
wrappedSigningKey: TEST_VECTOR_SIGNING_KEY_V2 as WrappedSigningKey,
verifyingKey: TEST_VECTOR_VERIFYING_KEY_V2 as VerifyingKey,
},
securityState: {
securityState: TEST_VECTOR_SECURITY_STATE_V2 as SignedSecurityState,
securityStateVersion: 2,
},
});
});
it("upgrades v1 user to v2 user", async () => {
mockMakeKeysForUserCryptoV2.mockReturnValue({
userKey: TEST_VECTOR_USER_KEY_V2.toBase64(),
privateKey: TEST_VECTOR_PRIVATE_KEY_V2,
publicKey: Utils.fromB64ToArray(TEST_VECTOR_PUBLIC_KEY_V2) as UnsignedPublicKey,
signedPublicKey: TEST_VECTOR_SIGNED_PUBLIC_KEY_V2,
signingKey: TEST_VECTOR_SIGNING_KEY_V2,
verifyingKey: TEST_VECTOR_VERIFYING_KEY_V2,
securityState: TEST_VECTOR_SECURITY_STATE_V2,
securityVersion: 2,
});
const result = await keyRotationService.getNewAccountKeysV2(
"00000000-0000-0000-0000-000000000000" as UserId,
new PBKDF2KdfConfig(600_000),
"mockuseremail",
{
version: 1,
userKey: TEST_VECTOR_USER_KEY_V1,
publicKeyEncryptionKeyPair: {
wrappedPrivateKey: TEST_VECTOR_PRIVATE_KEY_V1 as WrappedPrivateKey,
publicKey: Utils.fromB64ToArray(TEST_VECTOR_PUBLIC_KEY_V1) as UnsignedPublicKey,
},
},
);
expect(mockMakeKeysForUserCryptoV2).toHaveBeenCalled();
expect(result).toEqual({
userKey: TEST_VECTOR_USER_KEY_V2,
publicKeyEncryptionKeyPair: {
wrappedPrivateKey: TEST_VECTOR_PRIVATE_KEY_V2,
publicKey: Utils.fromB64ToArray(TEST_VECTOR_PUBLIC_KEY_V2),
signedPublicKey: TEST_VECTOR_SIGNED_PUBLIC_KEY_V2,
},
signatureKeyPair: {
wrappedSigningKey: TEST_VECTOR_SIGNING_KEY_V2 as WrappedSigningKey,
verifyingKey: TEST_VECTOR_VERIFYING_KEY_V2 as VerifyingKey,
},
securityState: {
securityState: TEST_VECTOR_SECURITY_STATE_V2 as SignedSecurityState,
securityStateVersion: 2,
},
});
}); });
}); });
@@ -560,7 +751,7 @@ describe("KeyRotationService", () => {
new SymmetricCryptoKey(new Uint8Array(32)) as MasterKey, new SymmetricCryptoKey(new Uint8Array(32)) as MasterKey,
); );
mockKeyService.hashMasterKey.mockResolvedValue("mockMasterPasswordHash"); mockKeyService.hashMasterKey.mockResolvedValue("mockMasterPasswordHash");
const newKey = new SymmetricCryptoKey(new Uint8Array(64)) as UserKey; const newKey = TEST_VECTOR_USER_KEY_V1;
const userAccount = mockUser; const userAccount = mockUser;
const masterPasswordUnlockData = const masterPasswordUnlockData =
await keyRotationService.createMasterPasswordUnlockDataRequest(newKey, { await keyRotationService.createMasterPasswordUnlockDataRequest(newKey, {
@@ -572,13 +763,13 @@ describe("KeyRotationService", () => {
expect(masterPasswordUnlockData).toEqual({ expect(masterPasswordUnlockData).toEqual({
masterKeyEncryptedUserKey: "mockNewUserKey", masterKeyEncryptedUserKey: "mockNewUserKey",
email: "mockEmail", email: "mockEmail",
kdfType: 0, kdfType: KdfType.PBKDF2_SHA256,
kdfIterations: 600_000, kdfIterations: 600_000,
masterKeyAuthenticationHash: "mockMasterPasswordHash", masterKeyAuthenticationHash: "mockMasterPasswordHash",
masterPasswordHint: "mockMasterPasswordHint", masterPasswordHint: "mockMasterPasswordHint",
}); });
expect(PureCrypto.encrypt_user_key_with_master_password).toHaveBeenCalledWith( expect(PureCrypto.encrypt_user_key_with_master_password).toHaveBeenCalledWith(
new SymmetricCryptoKey(new Uint8Array(64)).toEncoded(), TEST_VECTOR_USER_KEY_V1.toEncoded(),
"mockMasterPassword", "mockMasterPassword",
userAccount.email, userAccount.email,
new PBKDF2KdfConfig(600_000).toSdkConfig(), new PBKDF2KdfConfig(600_000).toSdkConfig(),
@@ -637,8 +828,8 @@ describe("KeyRotationService", () => {
masterKeyKdfConfig: new PBKDF2KdfConfig(600_000), masterKeyKdfConfig: new PBKDF2KdfConfig(600_000),
masterPasswordHint: "mockMasterPasswordHint", masterPasswordHint: "mockMasterPasswordHint",
}, },
[new Uint8Array(1)], // emergency access public key [new Uint8Array(1) as UnsignedPublicKey], // emergency access public key
[new Uint8Array(2)], // account recovery public key [new Uint8Array(2) as UnsignedPublicKey], // account recovery public key
); );
expect(accountUnlockDataRequest.passkeyUnlockData).toEqual([ expect(accountUnlockDataRequest.passkeyUnlockData).toEqual([
{ {
@@ -758,66 +949,29 @@ describe("KeyRotationService", () => {
expect(wasTrustDenied).toBe(true); expect(wasTrustDenied).toBe(true);
}); });
it("returns trusted keys if all dialogs are accepted", async () => { test.each([
KeyRotationTrustInfoComponent.open = initialPromptedOpenTrue; [[mockGranteeEmergencyAccessWithPublicKey], []],
EmergencyAccessTrustComponent.open = emergencyAccessTrustOpenTrusted; [[], [mockOrganizationUserResetPasswordEntry]],
AccountRecoveryTrustComponent.open = accountRecoveryTrustOpenTrusted; [[], []],
mockEmergencyAccessService.getPublicKeys.mockResolvedValue([ [[mockGranteeEmergencyAccessWithPublicKey], [mockOrganizationUserResetPasswordEntry]],
mockGranteeEmergencyAccessWithPublicKey, ])(
]); "returns trusted keys when dialogs are open and public keys are provided",
mockResetPasswordService.getPublicKeys.mockResolvedValue([ async (emUsers, orgs) => {
mockOrganizationUserResetPasswordEntry, KeyRotationTrustInfoComponent.open = initialPromptedOpenTrue;
]); EmergencyAccessTrustComponent.open = emergencyAccessTrustOpenTrusted;
const { AccountRecoveryTrustComponent.open = accountRecoveryTrustOpenTrusted;
wasTrustDenied, mockEmergencyAccessService.getPublicKeys.mockResolvedValue(emUsers);
trustedOrganizationPublicKeys: trustedOrgs, mockResetPasswordService.getPublicKeys.mockResolvedValue(orgs);
trustedEmergencyAccessUserPublicKeys: trustedEmergencyAccessUsers, const {
} = await keyRotationService.verifyTrust(mockUser); wasTrustDenied,
expect(wasTrustDenied).toBe(false); trustedOrganizationPublicKeys: trustedOrgs,
expect(trustedEmergencyAccessUsers).toEqual([ trustedEmergencyAccessUserPublicKeys: trustedEmergencyAccessUsers,
mockGranteeEmergencyAccessWithPublicKey.publicKey, } = await keyRotationService.verifyTrust(mockUser);
]); expect(wasTrustDenied).toBe(false);
expect(trustedOrgs).toEqual([mockOrganizationUserResetPasswordEntry.publicKey]); expect(trustedEmergencyAccessUsers).toEqual(emUsers.map((e) => e.publicKey));
}); expect(trustedOrgs).toEqual(orgs.map((o) => o.publicKey));
}); },
);
describe("makeNewUserKeyV1", () => {
it("throws if old keys is xchacha20poly1305 key", async () => {
await expect(
keyRotationService.makeNewUserKeyV1(new SymmetricCryptoKey(new Uint8Array(70)) as UserKey),
).rejects.toThrow(
"User account crypto format is v2, but the feature flag is disabled. User key rotation cannot proceed.",
);
});
it("returns new user key", async () => {
const oldKey = new SymmetricCryptoKey(new Uint8Array(64)) as UserKey;
const newKey = await keyRotationService.makeNewUserKeyV1(oldKey);
expect(newKey).toEqual(new SymmetricCryptoKey(new Uint8Array(64)));
});
});
describe("makeNewUserKeyV2", () => {
it("returns xchacha20poly1305 key", async () => {
const oldKey = new SymmetricCryptoKey(new Uint8Array(70)) as UserKey;
const { newUserKey } = await keyRotationService.makeNewUserKeyV2(oldKey);
expect(newUserKey).toEqual(new SymmetricCryptoKey(new Uint8Array(70)));
});
it("returns isUpgrading true if old key is v1", async () => {
const oldKey = new SymmetricCryptoKey(new Uint8Array(64)) as UserKey;
const newKey = await keyRotationService.makeNewUserKeyV2(oldKey);
expect(newKey).toEqual({
newUserKey: new SymmetricCryptoKey(new Uint8Array(70)),
isUpgrading: true,
});
});
it("returns isUpgrading false if old key is v2", async () => {
const oldKey = new SymmetricCryptoKey(new Uint8Array(70)) as UserKey;
const newKey = await keyRotationService.makeNewUserKeyV2(oldKey);
expect(newKey).toEqual({
newUserKey: new SymmetricCryptoKey(new Uint8Array(70)),
isUpgrading: false,
});
});
}); });
describe("getAccountDataRequest", () => { describe("getAccountDataRequest", () => {
@@ -890,13 +1044,264 @@ describe("KeyRotationService", () => {
}); });
describe("isV1UserKey", () => { describe("isV1UserKey", () => {
const v1Key = new SymmetricCryptoKey(new Uint8Array(64)); const aes256CbcHmacV1UserKey = new SymmetricCryptoKey(new Uint8Array(64));
const v2Key = new SymmetricCryptoKey(new Uint8Array(70)); const coseV2UserKey = new SymmetricCryptoKey(new Uint8Array(70));
it("returns true for v1 key", () => { it("returns true for v1 key", () => {
expect(keyRotationService.isV1User(v1Key as UserKey)).toBe(true); expect(keyRotationService.isV1User(aes256CbcHmacV1UserKey as UserKey)).toBe(true);
}); });
it("returns false for v2 key", () => { it("returns false for v2 key", () => {
expect(keyRotationService.isV1User(v2Key as UserKey)).toBe(false); expect(keyRotationService.isV1User(coseV2UserKey as UserKey)).toBe(false);
});
it("returns false for 32 byte AES256-CBC key", () => {
const aes256CbcKey = new SymmetricCryptoKey(new Uint8Array(32)) as UserKey;
expect(keyRotationService.isV1User(aes256CbcKey)).toBe(false);
});
});
describe("makeServerMasterKeyAuthenticationHash", () => {
it("returns the master key authentication hash", async () => {
mockKeyService.makeMasterKey.mockResolvedValue(
new SymmetricCryptoKey(new Uint8Array(32)) as MasterKey,
);
mockKeyService.hashMasterKey.mockResolvedValue("mockMasterPasswordHash");
const masterKeyAuthenticationHash =
await keyRotationService.makeServerMasterKeyAuthenticationHash(
"mockMasterPassword",
new PBKDF2KdfConfig(600_000),
"mockEmail",
);
expect(masterKeyAuthenticationHash).toBe("mockMasterPasswordHash");
expect(mockKeyService.makeMasterKey).toHaveBeenCalledWith(
"mockMasterPassword",
"mockEmail",
new PBKDF2KdfConfig(600_000),
);
expect(mockKeyService.hashMasterKey).toHaveBeenCalledWith(
"mockMasterPassword",
new SymmetricCryptoKey(new Uint8Array(32)) as MasterKey,
HashPurpose.ServerAuthorization,
);
});
});
describe("getCryptographicStateForUser", () => {
beforeEach(() => {
mockKdfConfigService.getKdfConfig$.mockReturnValue(
new BehaviorSubject(new PBKDF2KdfConfig(100000)),
);
mockKeyService.userKey$.mockReturnValue(new BehaviorSubject(TEST_VECTOR_USER_KEY_V2));
mockKeyService.userEncryptedPrivateKey$.mockReturnValue(
new BehaviorSubject(TEST_VECTOR_PRIVATE_KEY_V2 as string as EncryptedString),
);
mockKeyService.userSigningKey$.mockReturnValue(
new BehaviorSubject(TEST_VECTOR_SIGNING_KEY_V2 as WrappedSigningKey),
);
mockSecurityStateService.accountSecurityState$.mockReturnValue(
new BehaviorSubject(TEST_VECTOR_SECURITY_STATE_V2 as SignedSecurityState),
);
mockCryptoFunctionService.rsaExtractPublicKey.mockResolvedValue(
new Uint8Array(400) as UnsignedPublicKey,
);
});
it("returns the cryptographic state for v1 user", async () => {
mockKeyService.userKey$.mockReturnValue(
new BehaviorSubject(new SymmetricCryptoKey(new Uint8Array(64)) as UserKey),
);
mockKeyService.userEncryptedPrivateKey$.mockReturnValue(
new BehaviorSubject(TEST_VECTOR_PRIVATE_KEY_V1 as string as EncryptedString),
);
mockKeyService.userSigningKey$.mockReturnValue(new BehaviorSubject(null));
mockSecurityStateService.accountSecurityState$.mockReturnValue(new BehaviorSubject(null));
const cryptographicState = await keyRotationService.getCryptographicStateForUser(mockUser);
expect(cryptographicState).toEqual({
masterKeyKdfConfig: new PBKDF2KdfConfig(100000),
masterKeySalt: "mockemail", // the email is lowercased to become the salt
cryptographicStateParameters: {
version: 1,
userKey: TEST_VECTOR_USER_KEY_V1,
publicKeyEncryptionKeyPair: {
wrappedPrivateKey: TEST_VECTOR_PRIVATE_KEY_V1,
publicKey: Utils.fromB64ToArray(TEST_VECTOR_PUBLIC_KEY_V1),
},
},
});
});
it("returns the cryptographic state for v2 user", async () => {
const cryptographicState = await keyRotationService.getCryptographicStateForUser(mockUser);
expect(cryptographicState).toEqual({
masterKeyKdfConfig: new PBKDF2KdfConfig(100000),
masterKeySalt: "mockemail", // the email is lowercased to become the salt
cryptographicStateParameters: {
version: 2,
userKey: TEST_VECTOR_USER_KEY_V2,
publicKeyEncryptionKeyPair: {
wrappedPrivateKey: TEST_VECTOR_PRIVATE_KEY_V2,
publicKey: Utils.fromB64ToArray(TEST_VECTOR_PUBLIC_KEY_V2) as UnsignedPublicKey,
},
signingKey: TEST_VECTOR_SIGNING_KEY_V2 as WrappedSigningKey,
securityState: TEST_VECTOR_SECURITY_STATE_V2 as SignedSecurityState,
},
});
});
it("throws if no kdf config is found", async () => {
mockKdfConfigService.getKdfConfig$.mockReturnValue(new BehaviorSubject(null));
await expect(keyRotationService.getCryptographicStateForUser(mockUser)).rejects.toThrow(
"Failed to get KDF config",
);
});
it("throws if current user key is not found", async () => {
mockKeyService.userKey$.mockReturnValue(new BehaviorSubject(null));
await expect(keyRotationService.getCryptographicStateForUser(mockUser)).rejects.toThrow(
"Failed to get User key",
);
});
it("throws if private key is not found", async () => {
mockKeyService.userEncryptedPrivateKey$.mockReturnValue(new BehaviorSubject(null));
await expect(keyRotationService.getCryptographicStateForUser(mockUser)).rejects.toThrow(
"Failed to get Private key",
);
});
it("throws if user key is not AES256-CBC-HMAC or COSE", async () => {
const invalidKey = new SymmetricCryptoKey(new Uint8Array(32)) as UserKey;
mockKeyService.userKey$.mockReturnValue(new BehaviorSubject(invalidKey));
await expect(keyRotationService.getCryptographicStateForUser(mockUser)).rejects.toThrow(
"Unsupported user key type",
);
});
});
describe("getRotatedAccountKeysFlagged", () => {
const userId = "mockUserId" as UserId;
const kdfConfig = new PBKDF2KdfConfig(100000);
const masterKeySalt = "mockSalt";
const v1Params = {
version: 1,
userKey: TEST_VECTOR_USER_KEY_V1,
publicKeyEncryptionKeyPair: {
wrappedPrivateKey: TEST_VECTOR_PRIVATE_KEY_V1,
publicKey: Utils.fromB64ToArray(TEST_VECTOR_PUBLIC_KEY_V1) as UnsignedPublicKey,
},
} as V1CryptographicStateParameters;
const v2Params = {
version: 2,
userKey: TEST_VECTOR_USER_KEY_V2,
publicKeyEncryptionKeyPair: {
wrappedPrivateKey: TEST_VECTOR_PRIVATE_KEY_V2,
publicKey: Utils.fromB64ToArray(TEST_VECTOR_PUBLIC_KEY_V2) as UnsignedPublicKey,
},
signingKey: TEST_VECTOR_SIGNING_KEY_V2,
securityState: TEST_VECTOR_SECURITY_STATE_V2,
} as V2CryptographicStateParameters;
beforeEach(() => {
jest.spyOn(keyRotationService, "getNewAccountKeysV1").mockResolvedValue({
userKey: TEST_VECTOR_USER_KEY_V1,
publicKeyEncryptionKeyPair: {
wrappedPrivateKey: TEST_VECTOR_PRIVATE_KEY_V1_ROTATED,
publicKey: Utils.fromB64ToArray(TEST_VECTOR_PUBLIC_KEY_V1) as UnsignedPublicKey,
},
});
jest.spyOn(keyRotationService, "getNewAccountKeysV2").mockResolvedValue({
userKey: TEST_VECTOR_USER_KEY_V2,
publicKeyEncryptionKeyPair: {
wrappedPrivateKey: TEST_VECTOR_PRIVATE_KEY_V2,
publicKey: Utils.fromB64ToArray(TEST_VECTOR_PUBLIC_KEY_V2) as UnsignedPublicKey,
signedPublicKey: TEST_VECTOR_SIGNED_PUBLIC_KEY_V2,
},
signatureKeyPair: {
wrappedSigningKey: TEST_VECTOR_SIGNING_KEY_V2 as WrappedSigningKey,
verifyingKey: TEST_VECTOR_VERIFYING_KEY_V2 as VerifyingKey,
},
securityState: {
securityState: TEST_VECTOR_SECURITY_STATE_V2 as SignedSecurityState,
securityStateVersion: 2,
},
});
jest
.spyOn(AccountKeysRequest, "fromV1CryptographicState")
.mockReturnValue("v1Request" as any);
jest
.spyOn(AccountKeysRequest, "fromV2CryptographicState")
.mockResolvedValue("v2Request" as any);
});
it("returns v2 keys and request if v2UpgradeEnabled is true", async () => {
const result = await keyRotationService.getRotatedAccountKeysFlagged(
userId,
kdfConfig,
masterKeySalt,
v1Params,
true,
);
expect(keyRotationService.getNewAccountKeysV2).toHaveBeenCalledWith(
userId,
kdfConfig,
masterKeySalt,
v1Params,
);
expect(result).toEqual({
userKey: TEST_VECTOR_USER_KEY_V2,
accountKeysRequest: "v2Request",
});
});
it("returns v2 keys and request if params.version is 2", async () => {
const result = await keyRotationService.getRotatedAccountKeysFlagged(
userId,
kdfConfig,
masterKeySalt,
v2Params,
false,
);
expect(keyRotationService.getNewAccountKeysV2).toHaveBeenCalledWith(
userId,
kdfConfig,
masterKeySalt,
v2Params,
);
expect(result).toEqual({
userKey: TEST_VECTOR_USER_KEY_V2,
accountKeysRequest: "v2Request",
});
});
it("returns v1 keys and request if v2UpgradeEnabled is false and params.version is 1", async () => {
const result = await keyRotationService.getRotatedAccountKeysFlagged(
userId,
kdfConfig,
masterKeySalt,
v1Params,
false,
);
expect(keyRotationService.getNewAccountKeysV1).toHaveBeenCalledWith(v1Params);
expect(result).toEqual({
userKey: TEST_VECTOR_USER_KEY_V1,
accountKeysRequest: "v1Request",
});
});
});
describe("ensureIsAllowedToRotateUserKey", () => {
it("resolves if last sync exists", async () => {
mockSyncService.getLastSync.mockResolvedValue(new Date());
await expect(keyRotationService.ensureIsAllowedToRotateUserKey()).resolves.toBeUndefined();
});
it("throws if last sync is null", async () => {
mockSyncService.getLastSync.mockResolvedValue(null);
await expect(keyRotationService.ensureIsAllowedToRotateUserKey()).rejects.toThrow(
/de-synced|log out and log back in/i,
);
expect(mockLogService.info).toHaveBeenCalledWith(
"[Userkey rotation] Client was never synced. Aborting!",
);
}); });
}); });
}); });

View File

@@ -1,5 +1,5 @@
import { Injectable } from "@angular/core"; import { Injectable } from "@angular/core";
import { firstValueFrom } from "rxjs"; import { firstValueFrom, Observable } from "rxjs";
import { Account } from "@bitwarden/common/auth/abstractions/account.service"; import { Account } from "@bitwarden/common/auth/abstractions/account.service";
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum"; import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
@@ -7,13 +7,21 @@ import { CryptoFunctionService } from "@bitwarden/common/key-management/crypto/a
import { EncryptService } from "@bitwarden/common/key-management/crypto/abstractions/encrypt.service"; import { EncryptService } from "@bitwarden/common/key-management/crypto/abstractions/encrypt.service";
import { EncString } from "@bitwarden/common/key-management/crypto/models/enc-string"; import { EncString } from "@bitwarden/common/key-management/crypto/models/enc-string";
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 { firstValueFromOrThrow } from "@bitwarden/common/key-management/utils"; import { SecurityStateService } from "@bitwarden/common/key-management/security-state/abstractions/security-state.service";
import {
SignedSecurityState,
UnsignedPublicKey,
WrappedPrivateKey,
WrappedSigningKey,
} from "@bitwarden/common/key-management/types";
import { VaultTimeoutService } from "@bitwarden/common/key-management/vault-timeout"; import { VaultTimeoutService } from "@bitwarden/common/key-management/vault-timeout";
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
import { SdkClientFactory } from "@bitwarden/common/platform/abstractions/sdk/sdk-client-factory";
import { SdkLoadService } from "@bitwarden/common/platform/abstractions/sdk/sdk-load.service";
import { asUuid } from "@bitwarden/common/platform/abstractions/sdk/sdk.service";
import { EncryptionType, HashPurpose } from "@bitwarden/common/platform/enums"; import { EncryptionType, 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 { SymmetricCryptoKey } from "@bitwarden/common/platform/models/domain/symmetric-crypto-key";
import { SendService } from "@bitwarden/common/tools/send/services/send.service.abstraction"; import { SendService } from "@bitwarden/common/tools/send/services/send.service.abstraction";
import { UserId } from "@bitwarden/common/types/guid"; import { UserId } from "@bitwarden/common/types/guid";
@@ -28,7 +36,7 @@ import {
EmergencyAccessTrustComponent, EmergencyAccessTrustComponent,
KeyRotationTrustInfoComponent, KeyRotationTrustInfoComponent,
} from "@bitwarden/key-management-ui"; } from "@bitwarden/key-management-ui";
import { PureCrypto } from "@bitwarden/sdk-internal"; import { PureCrypto, TokenProvider } from "@bitwarden/sdk-internal";
import { OrganizationUserResetPasswordService } from "../../admin-console/organizations/members/services/organization-user-reset-password/organization-user-reset-password.service"; import { OrganizationUserResetPasswordService } from "../../admin-console/organizations/members/services/organization-user-reset-password/organization-user-reset-password.service";
import { WebauthnLoginAdminService } from "../../auth/core"; import { WebauthnLoginAdminService } from "../../auth/core";
@@ -39,6 +47,11 @@ import { MasterPasswordUnlockDataRequest } from "./request/master-password-unloc
import { RotateUserAccountKeysRequest } from "./request/rotate-user-account-keys.request"; import { RotateUserAccountKeysRequest } from "./request/rotate-user-account-keys.request";
import { UnlockDataRequest } from "./request/unlock-data.request"; import { UnlockDataRequest } from "./request/unlock-data.request";
import { UserDataRequest } from "./request/userdata.request"; import { UserDataRequest } from "./request/userdata.request";
import { V1UserCryptographicState } from "./types/v1-cryptographic-state";
import {
fromSdkV2KeysToV2UserCryptographicState,
V2UserCryptographicState,
} from "./types/v2-cryptographic-state";
import { UserKeyRotationApiService } from "./user-key-rotation-api.service"; import { UserKeyRotationApiService } from "./user-key-rotation-api.service";
type MasterPasswordAuthenticationAndUnlockData = { type MasterPasswordAuthenticationAndUnlockData = {
@@ -48,6 +61,19 @@ type MasterPasswordAuthenticationAndUnlockData = {
masterPasswordHint: string; masterPasswordHint: string;
}; };
/**
* A token provider that exposes a null access token to the SDK.
*/
class NoopTokenProvider implements TokenProvider {
constructor() {}
async get_access_token(): Promise<string | undefined> {
// Ignore from the test coverage, since this is called by the SDK
/* istanbul ignore next */
return undefined;
}
}
@Injectable({ providedIn: "root" }) @Injectable({ providedIn: "root" })
export class UserKeyRotationService { export class UserKeyRotationService {
constructor( constructor(
@@ -70,6 +96,8 @@ export class UserKeyRotationService {
private configService: ConfigService, private configService: ConfigService,
private cryptoFunctionService: CryptoFunctionService, private cryptoFunctionService: CryptoFunctionService,
private kdfConfigService: KdfConfigService, private kdfConfigService: KdfConfigService,
private sdkClientFactory: SdkClientFactory,
private securityStateService: SecurityStateService,
) {} ) {}
/** /**
@@ -85,12 +113,15 @@ export class UserKeyRotationService {
user: Account, user: Account,
newMasterPasswordHint?: string, newMasterPasswordHint?: string,
): Promise<void> { ): Promise<void> {
this.logService.info("[UserKey Rotation] Starting user key rotation..."); // Key-rotation uses the SDK, so we need to ensure that the SDK is loaded / the WASM initialized.
await SdkLoadService.Ready;
const upgradeToV2FeatureFlagEnabled = await this.configService.getFeatureFlag( const upgradeToV2FeatureFlagEnabled = await this.configService.getFeatureFlag(
FeatureFlag.EnrollAeadOnKeyRotation, FeatureFlag.EnrollAeadOnKeyRotation,
); );
this.logService.info("[UserKey Rotation] Starting user key rotation...");
// Make sure all conditions match - e.g. account state is up to date // Make sure all conditions match - e.g. account state is up to date
await this.ensureIsAllowedToRotateUserKey(); await this.ensureIsAllowedToRotateUserKey();
@@ -104,53 +135,26 @@ export class UserKeyRotationService {
} }
// Read current cryptographic state / settings // Read current cryptographic state / settings
const masterKeyKdfConfig: KdfConfig = (await firstValueFromOrThrow( const {
this.kdfConfigService.getKdfConfig$(user.id), masterKeyKdfConfig,
"KDF config", masterKeySalt,
))!; cryptographicStateParameters: currentCryptographicStateParameters,
// The masterkey salt used for deriving the masterkey always needs to be trimmed and lowercased. } = await this.getCryptographicStateForUser(user);
const masterKeySalt = user.email.trim().toLowerCase();
const currentUserKey: UserKey = (await firstValueFromOrThrow(
this.keyService.userKey$(user.id),
"User key",
))!;
const currentUserKeyWrappedPrivateKey = new EncString(
(await firstValueFromOrThrow(
this.keyService.userEncryptedPrivateKey$(user.id),
"User encrypted private key",
))!,
);
// Update account keys // Get new set of keys for the account.
// This creates at least a new user key, and possibly upgrades user encryption formats const { userKey: newUserKey, accountKeysRequest } = await this.getRotatedAccountKeysFlagged(
let newUserKey: UserKey; user.id,
let wrappedPrivateKey: EncString; masterKeyKdfConfig,
let publicKey: string; user.email,
if (upgradeToV2FeatureFlagEnabled) { currentCryptographicStateParameters,
this.logService.info("[Userkey rotation] Using v2 account keys"); upgradeToV2FeatureFlagEnabled,
const { userKey, asymmetricEncryptionKeys } = await this.getNewAccountKeysV2( );
currentUserKey,
currentUserKeyWrappedPrivateKey,
);
newUserKey = userKey;
wrappedPrivateKey = asymmetricEncryptionKeys.wrappedPrivateKey;
publicKey = asymmetricEncryptionKeys.publicKey;
} else {
this.logService.info("[Userkey rotation] Using v1 account keys");
const { userKey, asymmetricEncryptionKeys } = await this.getNewAccountKeysV1(
currentUserKey,
currentUserKeyWrappedPrivateKey,
);
newUserKey = userKey;
wrappedPrivateKey = asymmetricEncryptionKeys.wrappedPrivateKey;
publicKey = asymmetricEncryptionKeys.publicKey;
}
// Assemble the key rotation request // Assemble the key rotation request
const request = new RotateUserAccountKeysRequest( const request = new RotateUserAccountKeysRequest(
await this.getAccountUnlockDataRequest( await this.getAccountUnlockDataRequest(
user.id, user.id,
currentUserKey, currentCryptographicStateParameters.userKey,
newUserKey, newUserKey,
{ {
masterPassword: newMasterPassword, masterPassword: newMasterPassword,
@@ -161,8 +165,12 @@ export class UserKeyRotationService {
trustedEmergencyAccessUserPublicKeys, trustedEmergencyAccessUserPublicKeys,
trustedOrganizationPublicKeys, trustedOrganizationPublicKeys,
), ),
new AccountKeysRequest(wrappedPrivateKey.encryptedString!, publicKey), accountKeysRequest,
await this.getAccountDataRequest(currentUserKey, newUserKey, user), await this.getAccountDataRequest(
currentCryptographicStateParameters.userKey,
newUserKey,
user,
),
await this.makeServerMasterKeyAuthenticationHash( await this.makeServerMasterKeyAuthenticationHash(
currentMasterPassword, currentMasterPassword,
masterKeyKdfConfig, masterKeyKdfConfig,
@@ -194,55 +202,153 @@ export class UserKeyRotationService {
} }
} }
async getRotatedAccountKeysFlagged(
userId: UserId,
kdfConfig: KdfConfig,
masterKeySalt: string,
cryptographicStateParameters: V1CryptographicStateParameters | V2CryptographicStateParameters,
v2UpgradeEnabled: boolean,
): Promise<{ userKey: UserKey; accountKeysRequest: AccountKeysRequest }> {
if (v2UpgradeEnabled || cryptographicStateParameters.version === 2) {
const keys = await this.getNewAccountKeysV2(
userId,
kdfConfig,
masterKeySalt,
cryptographicStateParameters,
);
return {
userKey: keys.userKey,
accountKeysRequest: await AccountKeysRequest.fromV2CryptographicState(keys),
};
} else {
const keys = await this.getNewAccountKeysV1(
cryptographicStateParameters as V1CryptographicStateParameters,
);
return {
userKey: keys.userKey,
accountKeysRequest: AccountKeysRequest.fromV1CryptographicState(keys),
};
}
}
/**
* This method rotates the user key of a V1 user and re-encrypts the private key.
* @deprecated Removed after roll-out of V2 encryption.
*/
protected async getNewAccountKeysV1( protected async getNewAccountKeysV1(
currentUserKey: UserKey, cryptographicStateParameters: V1CryptographicStateParameters,
currentUserKeyWrappedPrivateKey: EncString, ): Promise<V1UserCryptographicState> {
): Promise<{ // Account key rotation creates a new user key. All downstream data and keys need to be re-encrypted under this key.
userKey: UserKey;
asymmetricEncryptionKeys: {
wrappedPrivateKey: EncString;
publicKey: string;
};
}> {
// Account key rotation creates a new userkey. All downstream data and keys need to be re-encrypted under this key.
// Further, this method is used to create new keys in the event that the key hierarchy changes, such as for the // Further, this method is used to create new keys in the event that the key hierarchy changes, such as for the
// creation of a new signing key pair. // creation of a new signing key pair.
const newUserKey = await this.makeNewUserKeyV1(currentUserKey); const newUserKey = new SymmetricCryptoKey(
PureCrypto.make_user_key_aes256_cbc_hmac(),
) as UserKey;
// Re-encrypt the private key with the new user key // Re-encrypt the private key with the new user key
// Rotation of the private key is not supported yet // Rotation of the private key is not supported yet
const privateKey = await this.encryptService.unwrapDecapsulationKey( const privateKey = await this.encryptService.unwrapDecapsulationKey(
currentUserKeyWrappedPrivateKey, new EncString(cryptographicStateParameters.publicKeyEncryptionKeyPair.wrappedPrivateKey),
currentUserKey, cryptographicStateParameters.userKey,
); );
const newUserKeyWrappedPrivateKey = await this.encryptService.wrapDecapsulationKey( const newUserKeyWrappedPrivateKey = (
await this.encryptService.wrapDecapsulationKey(privateKey, newUserKey)
).encryptedString! as string as WrappedPrivateKey;
const publicKey = (await this.cryptoFunctionService.rsaExtractPublicKey(
privateKey, privateKey,
newUserKey, )) as UnsignedPublicKey;
);
const publicKey = await this.cryptoFunctionService.rsaExtractPublicKey(privateKey);
return { return {
userKey: newUserKey, userKey: newUserKey,
asymmetricEncryptionKeys: { publicKeyEncryptionKeyPair: {
wrappedPrivateKey: newUserKeyWrappedPrivateKey, wrappedPrivateKey: newUserKeyWrappedPrivateKey,
publicKey: Utils.fromBufferToB64(publicKey), publicKey: publicKey,
}, },
}; };
} }
/**
* This method either enrolls a user from v1 encryption to v2 encryption, rotating the user key, or rotates the keys of a v2 user, staying on v2.
*/
protected async getNewAccountKeysV2( protected async getNewAccountKeysV2(
currentUserKey: UserKey, userId: UserId,
currentUserKeyWrappedPrivateKey: EncString, masterKeyKdfConfig: KdfConfig,
): Promise<{ masterKeySalt: string,
userKey: UserKey; cryptographicStateParameters: V1CryptographicStateParameters | V2CryptographicStateParameters,
asymmetricEncryptionKeys: { ): Promise<V2UserCryptographicState> {
wrappedPrivateKey: EncString; if (cryptographicStateParameters.version === 1) {
publicKey: string; return this.upgradeV1UserToV2UserAccountKeys(
}; userId,
}> { masterKeyKdfConfig,
throw new Error("User encryption v2 upgrade is not supported yet"); masterKeySalt,
cryptographicStateParameters as V1CryptographicStateParameters,
);
} else {
return this.rotateV2UserAccountKeys(
userId,
masterKeyKdfConfig,
masterKeySalt,
cryptographicStateParameters as V2CryptographicStateParameters,
);
}
} }
/**
* Upgrades a V1 user to a V2 user by creating a new user key, re-encrypting the private key, generating a signature key-pair, and
* finally creating a signed security state.
*/
protected async upgradeV1UserToV2UserAccountKeys(
userId: UserId,
kdfConfig: KdfConfig,
email: string,
cryptographicStateParameters: V1CryptographicStateParameters,
): Promise<V2UserCryptographicState> {
// Initialize an SDK with the current cryptographic state
const sdk = await this.sdkClientFactory.createSdkClient(new NoopTokenProvider());
await sdk.crypto().initialize_user_crypto({
userId: asUuid(userId),
kdfParams: kdfConfig.toSdkConfig(),
email: email,
privateKey: cryptographicStateParameters.publicKeyEncryptionKeyPair.wrappedPrivateKey,
signingKey: undefined,
securityState: undefined,
method: {
decryptedKey: { decrypted_user_key: cryptographicStateParameters.userKey.toBase64() },
},
});
return fromSdkV2KeysToV2UserCryptographicState(sdk.crypto().make_keys_for_user_crypto_v2());
}
/**
* Generates a new user key for a v2 user, and re-encrypts the private key, signing key.
*/
protected async rotateV2UserAccountKeys(
userId: UserId,
kdfConfig: KdfConfig,
email: string,
cryptographicStateParameters: V2CryptographicStateParameters,
): Promise<V2UserCryptographicState> {
// Initialize an SDK with the current cryptographic state
const sdk = await this.sdkClientFactory.createSdkClient(new NoopTokenProvider());
await sdk.crypto().initialize_user_crypto({
userId: asUuid(userId),
kdfParams: kdfConfig.toSdkConfig(),
email: email,
privateKey: cryptographicStateParameters.publicKeyEncryptionKeyPair.wrappedPrivateKey,
signingKey: cryptographicStateParameters.signingKey,
securityState: cryptographicStateParameters.securityState,
method: {
decryptedKey: { decrypted_user_key: cryptographicStateParameters.userKey.toBase64() },
},
});
return fromSdkV2KeysToV2UserCryptographicState(sdk.crypto().get_v2_rotated_account_keys());
}
/**
* Generates a new request for updating the master-password unlock/authentication data.
*/
protected async createMasterPasswordUnlockDataRequest( protected async createMasterPasswordUnlockDataRequest(
userKey: UserKey, userKey: UserKey,
newUnlockData: MasterPasswordAuthenticationAndUnlockData, newUnlockData: MasterPasswordAuthenticationAndUnlockData,
@@ -272,13 +378,17 @@ export class UserKeyRotationService {
); );
} }
/**
* Re-generates the accounts unlock methods, including master-password, passkey, trusted device, emergency access, and organization account recovery
* for the new user key.
*/
protected async getAccountUnlockDataRequest( protected async getAccountUnlockDataRequest(
userId: UserId, userId: UserId,
currentUserKey: UserKey, currentUserKey: UserKey,
newUserKey: UserKey, newUserKey: UserKey,
masterPasswordAuthenticationAndUnlockData: MasterPasswordAuthenticationAndUnlockData, masterPasswordAuthenticationAndUnlockData: MasterPasswordAuthenticationAndUnlockData,
trustedEmergencyAccessGranteesPublicKeys: Uint8Array[], trustedEmergencyAccessGranteesPublicKeys: UnsignedPublicKey[],
trustedOrganizationPublicKeys: Uint8Array[], trustedOrganizationPublicKeys: UnsignedPublicKey[],
): Promise<UnlockDataRequest> { ): Promise<UnlockDataRequest> {
// To ensure access; all unlock methods need to be updated and provided the new user key. // To ensure access; all unlock methods need to be updated and provided the new user key.
// User unlock methods // User unlock methods
@@ -321,10 +431,13 @@ export class UserKeyRotationService {
); );
} }
/**
* Verifies the trust of the organizations and emergency access users by prompting the user. Denying any of these will return early.
*/
protected async verifyTrust(user: Account): Promise<{ protected async verifyTrust(user: Account): Promise<{
wasTrustDenied: boolean; wasTrustDenied: boolean;
trustedOrganizationPublicKeys: Uint8Array[]; trustedOrganizationPublicKeys: UnsignedPublicKey[];
trustedEmergencyAccessUserPublicKeys: Uint8Array[]; trustedEmergencyAccessUserPublicKeys: UnsignedPublicKey[];
}> { }> {
// Since currently the joined organizations and emergency access grantees are // Since currently the joined organizations and emergency access grantees are
// not signed, manual trust prompts are required, to verify that the server // not signed, manual trust prompts are required, to verify that the server
@@ -392,11 +505,16 @@ export class UserKeyRotationService {
); );
return { return {
wasTrustDenied: false, wasTrustDenied: false,
trustedOrganizationPublicKeys: organizations.map((d) => d.publicKey), trustedOrganizationPublicKeys: organizations.map((d) => d.publicKey as UnsignedPublicKey),
trustedEmergencyAccessUserPublicKeys: emergencyAccessGrantees.map((d) => d.publicKey), trustedEmergencyAccessUserPublicKeys: emergencyAccessGrantees.map(
(d) => d.publicKey as UnsignedPublicKey,
),
}; };
} }
/**
* Re-encrypts the account data owned by the user, such as ciphers, folders, and sends with the new user key.
*/
protected async getAccountDataRequest( protected async getAccountDataRequest(
originalUserKey: UserKey, originalUserKey: UserKey,
newUnencryptedUserKey: UserKey, newUnencryptedUserKey: UserKey,
@@ -429,64 +547,6 @@ export class UserKeyRotationService {
return new UserDataRequest(rotatedCiphers, rotatedFolders, rotatedSends); return new UserDataRequest(rotatedCiphers, rotatedFolders, rotatedSends);
} }
protected async makeNewUserKeyV1(oldUserKey: UserKey): Promise<UserKey> {
// The user's account format is determined by the user key.
// Being tied to the userkey ensures an all-or-nothing approach. A compromised
// server cannot downgrade to a previous format (no signing keys) without
// completely making the account unusable.
//
// V0: AES256-CBC (no userkey, directly using masterkey) (pre-2019 accounts)
// This format is unsupported, and not secure; It is being forced migrated, and being removed
// V1: AES256-CBC-HMAC userkey, no signing key (2019-2025)
// This format is still supported, but may be migrated in the future
// V2: XChaCha20-Poly1305 userkey, signing key, account security version
// This is the new, modern format.
if (this.isV1User(oldUserKey)) {
this.logService.info(
"[Userkey rotation] Existing userkey key is AES256-CBC-HMAC; not upgrading",
);
return new SymmetricCryptoKey(PureCrypto.make_user_key_aes256_cbc_hmac()) as UserKey;
} else {
// If the feature flag is rolled back, we want to block rotation in order to be as safe as possible with the user's account.
this.logService.info(
"[Userkey rotation] Existing userkey key is XChaCha20-Poly1305, but feature flag is not enabled; aborting..",
);
throw new Error(
"User account crypto format is v2, but the feature flag is disabled. User key rotation cannot proceed.",
);
}
}
protected async makeNewUserKeyV2(
oldUserKey: UserKey,
): Promise<{ isUpgrading: boolean; newUserKey: UserKey }> {
// The user's account format is determined by the user key.
// Being tied to the userkey ensures an all-or-nothing approach. A compromised
// server cannot downgrade to a previous format (no signing keys) without
// completely making the account unusable.
//
// V0: AES256-CBC (no userkey, directly using masterkey) (pre-2019 accounts)
// This format is unsupported, and not secure; It is being forced migrated, and being removed
// V1: AES256-CBC-HMAC userkey, no signing key (2019-2025)
// This format is still supported, but may be migrated in the future
// V2: XChaCha20-Poly1305 userkey, signing key, account security version
// This is the new, modern format.
const newUserKey: UserKey = new SymmetricCryptoKey(
PureCrypto.make_user_key_xchacha20_poly1305(),
) as UserKey;
const isUpgrading = this.isV1User(oldUserKey);
if (isUpgrading) {
this.logService.info(
"[Userkey rotation] Existing userkey key is AES256-CBC-HMAC; upgrading to XChaCha20-Poly1305",
);
} else {
this.logService.info(
"[Userkey rotation] Existing userkey key is XChaCha20-Poly1305; no upgrade needed",
);
}
return { isUpgrading, newUserKey };
}
/** /**
* A V1 user has no signing key, and uses AES256-CBC-HMAC. * A V1 user has no signing key, and uses AES256-CBC-HMAC.
* A V2 user has a signing key, and uses XChaCha20-Poly1305. * A V2 user has a signing key, and uses XChaCha20-Poly1305.
@@ -516,4 +576,111 @@ export class UserKeyRotationService {
HashPurpose.ServerAuthorization, HashPurpose.ServerAuthorization,
); );
} }
/**
* Gets the cryptographic state for a user. This can be a V1 user or a V2 user.
*/
protected async getCryptographicStateForUser(user: Account): Promise<{
masterKeyKdfConfig: KdfConfig;
masterKeySalt: string;
cryptographicStateParameters: V1CryptographicStateParameters | V2CryptographicStateParameters;
}> {
// Master password unlock
const masterKeyKdfConfig: KdfConfig = (await this.firstValueFromOrThrow(
this.kdfConfigService.getKdfConfig$(user.id),
"KDF config",
))!;
// The master key salt used for deriving the masterkey always needs to be trimmed and lowercased.
const masterKeySalt = user.email.trim().toLowerCase();
// V1 and V2 users both have a user key and a private key
const currentUserKey: UserKey = (await this.firstValueFromOrThrow(
this.keyService.userKey$(user.id),
"User key",
))!;
const currentUserKeyWrappedPrivateKey: WrappedPrivateKey = new EncString(
(await this.firstValueFromOrThrow(
this.keyService.userEncryptedPrivateKey$(user.id),
"Private key",
))!,
).encryptedString! as string as WrappedPrivateKey;
const publicKey = (await this.cryptoFunctionService.rsaExtractPublicKey(
await this.encryptService.unwrapDecapsulationKey(
new EncString(currentUserKeyWrappedPrivateKey),
currentUserKey,
),
)) as UnsignedPublicKey;
if (this.isV1User(currentUserKey)) {
return {
masterKeyKdfConfig,
masterKeySalt,
cryptographicStateParameters: {
version: 1,
userKey: currentUserKey,
publicKeyEncryptionKeyPair: {
wrappedPrivateKey: currentUserKeyWrappedPrivateKey,
publicKey: publicKey,
},
},
};
} else if (currentUserKey.inner().type === EncryptionType.CoseEncrypt0) {
const signingKey = await this.firstValueFromOrThrow(
this.keyService.userSigningKey$(user.id),
"User signing key",
);
const securityState = await this.firstValueFromOrThrow(
this.securityStateService.accountSecurityState$(user.id),
"User security state",
);
return {
masterKeyKdfConfig,
masterKeySalt,
cryptographicStateParameters: {
version: 2,
userKey: currentUserKey,
publicKeyEncryptionKeyPair: {
wrappedPrivateKey: currentUserKeyWrappedPrivateKey,
publicKey: publicKey,
},
signingKey: signingKey!,
securityState: securityState!,
},
};
}
/// AES-CBC (no-hmac) keys are not supported as user keys
throw new Error(
`Unsupported user key type: ${currentUserKey.inner().type}. Expected AesCbc256_HmacSha256_B64 or XChaCha20_Poly1305_B64.`,
);
}
async firstValueFromOrThrow<T>(value: Observable<T>, name: string): Promise<T> {
const result = await firstValueFrom(value);
if (result == null) {
throw new Error(`Failed to get ${name}`);
}
return result as T;
}
} }
export type V1CryptographicStateParameters = {
version: 1;
userKey: UserKey;
publicKeyEncryptionKeyPair: {
wrappedPrivateKey: WrappedPrivateKey;
publicKey: UnsignedPublicKey;
};
};
export type V2CryptographicStateParameters = {
version: 2;
userKey: UserKey;
publicKeyEncryptionKeyPair: {
wrappedPrivateKey: WrappedPrivateKey;
publicKey: UnsignedPublicKey;
};
signingKey: WrappedSigningKey;
securityState: SignedSecurityState;
};

View File

@@ -170,6 +170,8 @@ import { DefaultChangeKdfService } from "@bitwarden/common/key-management/kdf/ch
import { ChangeKdfService } from "@bitwarden/common/key-management/kdf/change-kdf-service.abstraction"; import { ChangeKdfService } from "@bitwarden/common/key-management/kdf/change-kdf-service.abstraction";
import { KeyConnectorService as KeyConnectorServiceAbstraction } from "@bitwarden/common/key-management/key-connector/abstractions/key-connector.service"; import { KeyConnectorService as KeyConnectorServiceAbstraction } from "@bitwarden/common/key-management/key-connector/abstractions/key-connector.service";
import { KeyConnectorService } from "@bitwarden/common/key-management/key-connector/services/key-connector.service"; import { KeyConnectorService } from "@bitwarden/common/key-management/key-connector/services/key-connector.service";
import { KeyApiService } from "@bitwarden/common/key-management/keys/services/abstractions/key-api-service.abstraction";
import { DefaultKeyApiService } from "@bitwarden/common/key-management/keys/services/default-key-api-service.service";
import { import {
InternalMasterPasswordServiceAbstraction, InternalMasterPasswordServiceAbstraction,
MasterPasswordServiceAbstraction, MasterPasswordServiceAbstraction,
@@ -177,6 +179,8 @@ import {
import { MasterPasswordService } from "@bitwarden/common/key-management/master-password/services/master-password.service"; import { MasterPasswordService } from "@bitwarden/common/key-management/master-password/services/master-password.service";
import { PinServiceAbstraction } from "@bitwarden/common/key-management/pin/pin.service.abstraction"; import { PinServiceAbstraction } from "@bitwarden/common/key-management/pin/pin.service.abstraction";
import { PinService } from "@bitwarden/common/key-management/pin/pin.service.implementation"; import { PinService } from "@bitwarden/common/key-management/pin/pin.service.implementation";
import { SecurityStateService } from "@bitwarden/common/key-management/security-state/abstractions/security-state.service";
import { DefaultSecurityStateService } from "@bitwarden/common/key-management/security-state/services/security-state.service";
import { import {
SendPasswordService, SendPasswordService,
DefaultSendPasswordService, DefaultSendPasswordService,
@@ -702,6 +706,11 @@ const safeProviders: SafeProvider[] = [
KdfConfigService, KdfConfigService,
], ],
}), }),
safeProvider({
provide: SecurityStateService,
useClass: DefaultSecurityStateService,
deps: [StateProvider],
}),
safeProvider({ safeProvider({
provide: RestrictedItemTypesService, provide: RestrictedItemTypesService,
useClass: RestrictedItemTypesService, useClass: RestrictedItemTypesService,
@@ -797,6 +806,11 @@ const safeProviders: SafeProvider[] = [
useClass: SendApiService, useClass: SendApiService,
deps: [ApiServiceAbstraction, FileUploadServiceAbstraction, InternalSendService], deps: [ApiServiceAbstraction, FileUploadServiceAbstraction, InternalSendService],
}), }),
safeProvider({
provide: KeyApiService,
useClass: DefaultKeyApiService,
deps: [ApiServiceAbstraction],
}),
safeProvider({ safeProvider({
provide: SyncService, provide: SyncService,
useClass: DefaultSyncService, useClass: DefaultSyncService,
@@ -825,6 +839,7 @@ const safeProviders: SafeProvider[] = [
TokenServiceAbstraction, TokenServiceAbstraction,
AuthServiceAbstraction, AuthServiceAbstraction,
StateProvider, StateProvider,
SecurityStateService,
], ],
}), }),
safeProvider({ safeProvider({
@@ -1523,6 +1538,7 @@ const safeProviders: SafeProvider[] = [
AccountServiceAbstraction, AccountServiceAbstraction,
KdfConfigService, KdfConfigService,
KeyService, KeyService,
SecurityStateService,
ApiServiceAbstraction, ApiServiceAbstraction,
StateProvider, StateProvider,
ConfigService, ConfigService,

View File

@@ -8,6 +8,7 @@ import {
} from "../../../platform/models/domain/decrypt-parameters"; } from "../../../platform/models/domain/decrypt-parameters";
import { SymmetricCryptoKey } from "../../../platform/models/domain/symmetric-crypto-key"; import { SymmetricCryptoKey } from "../../../platform/models/domain/symmetric-crypto-key";
import { CsprngArray } from "../../../types/csprng"; import { CsprngArray } from "../../../types/csprng";
import { UnsignedPublicKey } from "../../types";
import { CryptoFunctionService } from "../abstractions/crypto-function.service"; import { CryptoFunctionService } from "../abstractions/crypto-function.service";
export class WebCryptoFunctionService implements CryptoFunctionService { export class WebCryptoFunctionService implements CryptoFunctionService {
@@ -309,7 +310,7 @@ export class WebCryptoFunctionService implements CryptoFunctionService {
"encrypt", "encrypt",
]); ]);
const buffer = await this.subtle.exportKey("spki", impPublicKey); const buffer = await this.subtle.exportKey("spki", impPublicKey);
return new Uint8Array(buffer); return new Uint8Array(buffer) as UnsignedPublicKey;
} }
async aesGenerateKey(bitLength = 128 | 192 | 256 | 512): Promise<CsprngArray> { async aesGenerateKey(bitLength = 128 | 192 | 256 | 512): Promise<CsprngArray> {

View File

@@ -0,0 +1,13 @@
export const SigningKeyTypes = {
Ed25519: "ed25519",
} as const;
export type SigningKeyType = (typeof SigningKeyTypes)[keyof typeof SigningKeyTypes];
export function parseSigningKeyTypeFromString(value: string): SigningKeyType {
switch (value) {
case SigningKeyTypes.Ed25519:
return SigningKeyTypes.Ed25519;
default:
throw new Error(`Unknown signing key type: ${value}`);
}
}

View File

@@ -0,0 +1,55 @@
import { SecurityStateResponse } from "../../security-state/response/security-state.response";
import { PublicKeyEncryptionKeyPairResponse } from "./public-key-encryption-key-pair.response";
import { SignatureKeyPairResponse } from "./signature-key-pair.response";
/**
* The privately accessible view of an entity (account / org)'s keys.
* This includes the full key-pairs for public-key encryption and signing, as well as the security state if available.
*/
export class PrivateKeysResponseModel {
readonly publicKeyEncryptionKeyPair: PublicKeyEncryptionKeyPairResponse;
readonly signatureKeyPair: SignatureKeyPairResponse | null = null;
readonly securityState: SecurityStateResponse | null = null;
constructor(response: unknown) {
if (typeof response !== "object" || response == null) {
throw new TypeError("Response must be an object");
}
if (
!("publicKeyEncryptionKeyPair" in response) ||
typeof response.publicKeyEncryptionKeyPair !== "object"
) {
throw new TypeError("Response must contain a valid publicKeyEncryptionKeyPair");
}
this.publicKeyEncryptionKeyPair = new PublicKeyEncryptionKeyPairResponse(
response.publicKeyEncryptionKeyPair,
);
if (
"signatureKeyPair" in response &&
typeof response.signatureKeyPair === "object" &&
response.signatureKeyPair != null
) {
this.signatureKeyPair = new SignatureKeyPairResponse(response.signatureKeyPair);
}
if (
"securityState" in response &&
typeof response.securityState === "object" &&
response.securityState != null
) {
this.securityState = new SecurityStateResponse(response.securityState);
}
if (
(this.signatureKeyPair !== null && this.securityState === null) ||
(this.signatureKeyPair === null && this.securityState !== null)
) {
throw new TypeError(
"Both signatureKeyPair and securityState must be present or absent together",
);
}
}
}

View File

@@ -0,0 +1,32 @@
import { Utils } from "@bitwarden/common/platform/misc/utils";
import { SignedPublicKey, UnsignedPublicKey, WrappedPrivateKey } from "../../types";
export class PublicKeyEncryptionKeyPairResponse {
readonly wrappedPrivateKey: WrappedPrivateKey;
readonly publicKey: UnsignedPublicKey;
readonly signedPublicKey: SignedPublicKey | null = null;
constructor(response: unknown) {
if (typeof response !== "object" || response == null) {
throw new TypeError("Response must be an object");
}
if (!("publicKey" in response) || typeof response.publicKey !== "string") {
throw new TypeError("Response must contain a valid publicKey");
}
this.publicKey = Utils.fromB64ToArray(response.publicKey) as UnsignedPublicKey;
if (!("wrappedPrivateKey" in response) || typeof response.wrappedPrivateKey !== "string") {
throw new TypeError("Response must contain a valid wrappedPrivateKey");
}
this.wrappedPrivateKey = response.wrappedPrivateKey as WrappedPrivateKey;
if ("signedPublicKey" in response && typeof response.signedPublicKey === "string") {
this.signedPublicKey = response.signedPublicKey as SignedPublicKey;
} else {
this.signedPublicKey = null;
}
}
}

View File

@@ -0,0 +1,44 @@
import { SignedPublicKey } from "@bitwarden/sdk-internal";
import { UnsignedPublicKey, VerifyingKey } from "../../types";
/**
* The publicly accessible view of an entity (account / org)'s keys. That includes the encryption public key, and the verifying key if available.
*/
export class PublicKeysResponseModel {
readonly publicKey: UnsignedPublicKey;
readonly verifyingKey: VerifyingKey | null;
readonly signedPublicKey?: SignedPublicKey | null;
constructor(response: unknown) {
if (typeof response !== "object" || response == null) {
throw new TypeError("Response must be an object");
}
if (!("publicKey" in response) || !(response.publicKey instanceof Uint8Array)) {
throw new TypeError("Response must contain a valid publicKey");
}
this.publicKey = response.publicKey as UnsignedPublicKey;
if ("verifyingKey" in response && typeof response.verifyingKey === "string") {
this.verifyingKey = response.verifyingKey as VerifyingKey;
} else {
this.verifyingKey = null;
}
if ("signedPublicKey" in response && typeof response.signedPublicKey === "string") {
this.signedPublicKey = response.signedPublicKey as SignedPublicKey;
} else {
this.signedPublicKey = null;
}
if (
(this.signedPublicKey !== null && this.verifyingKey === null) ||
(this.signedPublicKey === null && this.verifyingKey !== null)
) {
throw new TypeError(
"Both signedPublicKey and verifyingKey must be present or absent together",
);
}
}
}

View File

@@ -0,0 +1,22 @@
import { VerifyingKey, WrappedSigningKey } from "../../types";
export class SignatureKeyPairResponse {
readonly wrappedSigningKey: WrappedSigningKey;
readonly verifyingKey: VerifyingKey;
constructor(response: unknown) {
if (typeof response !== "object" || response == null) {
throw new TypeError("Response must be an object");
}
if (!("wrappedSigningKey" in response) || typeof response.wrappedSigningKey !== "string") {
throw new TypeError("Response must contain a valid wrappedSigningKey");
}
this.wrappedSigningKey = response.wrappedSigningKey as WrappedSigningKey;
if (!("verifyingKey" in response) || typeof response.verifyingKey !== "string") {
throw new TypeError("Response must contain a valid verifyingKey");
}
this.verifyingKey = response.verifyingKey as VerifyingKey;
}
}

View File

@@ -0,0 +1,5 @@
import { PublicKeysResponseModel } from "../../response/public-keys.response";
export abstract class KeyApiService {
abstract getUserPublicKeys(id: string): Promise<PublicKeysResponseModel>;
}

View File

@@ -0,0 +1,15 @@
import { UserId } from "@bitwarden/common/types/guid";
import { ApiService } from "../../../abstractions/api.service";
import { PublicKeysResponseModel } from "../response/public-keys.response";
import { KeyApiService } from "./abstractions/key-api-service.abstraction";
export class DefaultKeyApiService implements KeyApiService {
constructor(private apiService: ApiService) {}
async getUserPublicKeys(id: UserId): Promise<PublicKeysResponseModel> {
const response = await this.apiService.send("GET", "/users/" + id + "/keys", null, true, true);
return new PublicKeysResponseModel(response);
}
}

View File

@@ -0,0 +1,21 @@
import { Observable } from "rxjs";
import { UserId } from "@bitwarden/common/types/guid";
import { SignedSecurityState } from "../../types";
export abstract class SecurityStateService {
/**
* Retrieves the security state for the provided user.
* Note: This state is not yet validated. To get a validated state, the SDK crypto client
* must be used. This security state is validated on initialization of the SDK.
*/
abstract accountSecurityState$(userId: UserId): Observable<SignedSecurityState | null>;
/**
* Sets the security state for the provided user.
*/
abstract setAccountSecurityState(
accountSecurityState: SignedSecurityState,
userId: UserId,
): Promise<void>;
}

View File

@@ -0,0 +1,8 @@
import { SignedSecurityState } from "../../types";
export class SecurityStateRequest {
constructor(
readonly securityState: SignedSecurityState,
readonly securityVersion: number,
) {}
}

View File

@@ -0,0 +1,16 @@
import { SignedSecurityState } from "../../types";
export class SecurityStateResponse {
readonly securityState: SignedSecurityState | null = null;
constructor(response: unknown) {
if (typeof response !== "object" || response == null) {
throw new TypeError("Response must be an object");
}
if (!("securityState" in response) || !(typeof response.securityState === "string")) {
throw new TypeError("Response must contain a valid securityState");
}
this.securityState = response.securityState as SignedSecurityState;
}
}

View File

@@ -0,0 +1,26 @@
import { Observable } from "rxjs";
import { StateProvider } from "@bitwarden/common/platform/state";
import { UserId } from "@bitwarden/common/types/guid";
import { SignedSecurityState } from "../../types";
import { SecurityStateService } from "../abstractions/security-state.service";
import { ACCOUNT_SECURITY_STATE } from "../state/security-state.state";
export class DefaultSecurityStateService implements SecurityStateService {
constructor(protected stateProvider: StateProvider) {}
// Emits the provided user's security state, or null if there is no security state present for the user.
accountSecurityState$(userId: UserId): Observable<SignedSecurityState | null> {
return this.stateProvider.getUserState$(ACCOUNT_SECURITY_STATE, userId);
}
// Sets the security state for the provided user.
// This is not yet validated, and is only validated upon SDK initialization.
async setAccountSecurityState(
accountSecurityState: SignedSecurityState,
userId: UserId,
): Promise<void> {
await this.stateProvider.setUserState(ACCOUNT_SECURITY_STATE, accountSecurityState, userId);
}
}

View File

@@ -0,0 +1,12 @@
import { CRYPTO_DISK, UserKeyDefinition } from "@bitwarden/common/platform/state";
import { SignedSecurityState } from "../../types";
export const ACCOUNT_SECURITY_STATE = new UserKeyDefinition<SignedSecurityState>(
CRYPTO_DISK,
"accountSecurityState",
{
deserializer: (obj) => obj,
clearOn: ["logout"],
},
);

View File

@@ -0,0 +1,30 @@
import { Opaque } from "type-fest";
import { EncString, SignedSecurityState as SdkSignedSecurityState } from "@bitwarden/sdk-internal";
/**
* A private key, encrypted with a symmetric key.
*/
export type WrappedPrivateKey = Opaque<EncString, "WrappedPrivateKey">;
/**
* A public key, signed with the accounts signature key.
*/
export type SignedPublicKey = Opaque<string, "SignedPublicKey">;
/**
* A public key in base64 encoded SPKI-DER
*/
export type UnsignedPublicKey = Opaque<Uint8Array, "UnsignedPublicKey">;
/**
* A signature key encrypted with a symmetric key.
*/
export type WrappedSigningKey = Opaque<EncString, "WrappedSigningKey">;
/**
* A signature public key (verifying key) in base64 encoded CoseKey format
*/
export type VerifyingKey = Opaque<string, "VerifyingKey">;
/**
* A signed security state, encoded in base64.
*/
export type SignedSecurityState = Opaque<SdkSignedSecurityState, "SignedSecurityState">;

View File

@@ -1,3 +1,5 @@
import { PrivateKeysResponseModel } from "@bitwarden/common/key-management/keys/response/private-keys.response";
import { ProfileOrganizationResponse } from "../../admin-console/models/response/profile-organization.response"; import { ProfileOrganizationResponse } from "../../admin-console/models/response/profile-organization.response";
import { ProfileProviderOrganizationResponse } from "../../admin-console/models/response/profile-provider-organization.response"; import { ProfileProviderOrganizationResponse } from "../../admin-console/models/response/profile-provider-organization.response";
import { ProfileProviderResponse } from "../../admin-console/models/response/profile-provider.response"; import { ProfileProviderResponse } from "../../admin-console/models/response/profile-provider.response";
@@ -18,7 +20,10 @@ export class ProfileResponse extends BaseResponse {
key?: EncString; key?: EncString;
avatarColor: string; avatarColor: string;
creationDate: string; creationDate: string;
// Cleanup: Can be removed after moving to accountKeys
privateKey: string; privateKey: string;
// Cleanup: This should be non-optional after the server has been released for a while https://bitwarden.atlassian.net/browse/PM-21768
accountKeys: PrivateKeysResponseModel | null = null;
securityStamp: string; securityStamp: string;
forcePasswordReset: boolean; forcePasswordReset: boolean;
usesKeyConnector: boolean; usesKeyConnector: boolean;
@@ -37,10 +42,16 @@ export class ProfileResponse extends BaseResponse {
this.premiumFromOrganization = this.getResponseProperty("PremiumFromOrganization"); this.premiumFromOrganization = this.getResponseProperty("PremiumFromOrganization");
this.culture = this.getResponseProperty("Culture"); this.culture = this.getResponseProperty("Culture");
this.twoFactorEnabled = this.getResponseProperty("TwoFactorEnabled"); this.twoFactorEnabled = this.getResponseProperty("TwoFactorEnabled");
const key = this.getResponseProperty("Key"); const key = this.getResponseProperty("Key");
if (key) { if (key) {
this.key = new EncString(key); this.key = new EncString(key);
} }
// Cleanup: This should be non-optional after the server has been released for a while https://bitwarden.atlassian.net/browse/PM-21768
if (this.getResponseProperty("AccountKeys") != null) {
this.accountKeys = new PrivateKeysResponseModel(this.getResponseProperty("AccountKeys"));
}
this.avatarColor = this.getResponseProperty("AvatarColor"); this.avatarColor = this.getResponseProperty("AvatarColor");
this.creationDate = this.getResponseProperty("CreationDate"); this.creationDate = this.getResponseProperty("CreationDate");
this.privateKey = this.getResponseProperty("PrivateKey"); this.privateKey = this.getResponseProperty("PrivateKey");

View File

@@ -1,4 +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 { 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";
@@ -25,3 +26,12 @@ export const USER_KEY = new UserKeyDefinition<UserKey>(CRYPTO_MEMORY, "userKey",
deserializer: (obj) => SymmetricCryptoKey.fromJSON(obj) as UserKey, deserializer: (obj) => SymmetricCryptoKey.fromJSON(obj) as UserKey,
clearOn: ["logout", "lock"], clearOn: ["logout", "lock"],
}); });
export const USER_KEY_ENCRYPTED_SIGNING_KEY = new UserKeyDefinition<WrappedSigningKey>(
CRYPTO_DISK,
"userSigningKey",
{
deserializer: (obj) => obj,
clearOn: ["logout"],
},
);

View File

@@ -1,7 +1,7 @@
import { mock, MockProxy } from "jest-mock-extended"; import { mock, MockProxy } from "jest-mock-extended";
import { BehaviorSubject, firstValueFrom, of } from "rxjs"; import { BehaviorSubject, firstValueFrom, of } from "rxjs";
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.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. // 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 { KdfConfigService, KeyService, PBKDF2KdfConfig } from "@bitwarden/key-management"; import { KdfConfigService, KeyService, PBKDF2KdfConfig } from "@bitwarden/key-management";
@@ -18,6 +18,7 @@ import { AccountInfo } from "../../../auth/abstractions/account.service";
import { EncryptedString } from "../../../key-management/crypto/models/enc-string"; import { EncryptedString } from "../../../key-management/crypto/models/enc-string";
import { UserId } from "../../../types/guid"; import { UserId } from "../../../types/guid";
import { UserKey } from "../../../types/key"; import { UserKey } from "../../../types/key";
import { ConfigService } from "../../abstractions/config/config.service";
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";
@@ -43,6 +44,7 @@ describe("DefaultSdkService", () => {
let platformUtilsService!: MockProxy<PlatformUtilsService>; let platformUtilsService!: MockProxy<PlatformUtilsService>;
let kdfConfigService!: MockProxy<KdfConfigService>; let kdfConfigService!: MockProxy<KdfConfigService>;
let keyService!: MockProxy<KeyService>; let keyService!: MockProxy<KeyService>;
let securityStateService!: MockProxy<SecurityStateService>;
let configService!: MockProxy<ConfigService>; let configService!: MockProxy<ConfigService>;
let service!: DefaultSdkService; let service!: DefaultSdkService;
let accountService!: FakeAccountService; let accountService!: FakeAccountService;
@@ -57,6 +59,7 @@ describe("DefaultSdkService", () => {
platformUtilsService = mock<PlatformUtilsService>(); platformUtilsService = mock<PlatformUtilsService>();
kdfConfigService = mock<KdfConfigService>(); kdfConfigService = mock<KdfConfigService>();
keyService = mock<KeyService>(); keyService = mock<KeyService>();
securityStateService = mock<SecurityStateService>();
apiService = mock<ApiService>(); apiService = mock<ApiService>();
const mockUserId = Utils.newGuid() as UserId; const mockUserId = Utils.newGuid() as UserId;
accountService = mockAccountServiceWith(mockUserId); accountService = mockAccountServiceWith(mockUserId);
@@ -75,6 +78,7 @@ describe("DefaultSdkService", () => {
accountService, accountService,
kdfConfigService, kdfConfigService,
keyService, keyService,
securityStateService,
apiService, apiService,
fakeStateProvider, fakeStateProvider,
configService, configService,
@@ -100,6 +104,8 @@ describe("DefaultSdkService", () => {
.calledWith(userId) .calledWith(userId)
.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));
securityStateService.accountSecurityState$.calledWith(userId).mockReturnValue(of(null));
}); });
describe("given no client override has been set for the user", () => { describe("given no client override has been set for the user", () => {

View File

@@ -31,6 +31,8 @@ import { ApiService } from "../../../abstractions/api.service";
import { AccountInfo, AccountService } from "../../../auth/abstractions/account.service"; import { AccountInfo, AccountService } from "../../../auth/abstractions/account.service";
import { DeviceType } from "../../../enums/device-type.enum"; import { DeviceType } from "../../../enums/device-type.enum";
import { EncryptedString, EncString } from "../../../key-management/crypto/models/enc-string"; import { EncryptedString, 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 { OrganizationId, UserId } from "../../../types/guid";
import { UserKey } from "../../../types/key"; import { UserKey } from "../../../types/key";
import { Environment, EnvironmentService } from "../../abstractions/environment.service"; import { Environment, EnvironmentService } from "../../abstractions/environment.service";
@@ -98,6 +100,7 @@ export class DefaultSdkService implements SdkService {
private accountService: AccountService, private accountService: AccountService,
private kdfConfigService: KdfConfigService, private kdfConfigService: KdfConfigService,
private keyService: KeyService, private keyService: KeyService,
private securityStateService: SecurityStateService,
private apiService: ApiService, private apiService: ApiService,
private stateProvider: StateProvider, private stateProvider: StateProvider,
private configService: ConfigService, private configService: ConfigService,
@@ -160,10 +163,14 @@ export class DefaultSdkService implements SdkService {
const privateKey$ = this.keyService const privateKey$ = this.keyService
.userEncryptedPrivateKey$(userId) .userEncryptedPrivateKey$(userId)
.pipe(distinctUntilChanged()); .pipe(distinctUntilChanged());
const signingKey$ = this.keyService.userSigningKey$(userId).pipe(distinctUntilChanged());
const userKey$ = this.keyService.userKey$(userId).pipe(distinctUntilChanged()); const userKey$ = this.keyService.userKey$(userId).pipe(distinctUntilChanged());
const orgKeys$ = this.keyService.encryptedOrgKeys$(userId).pipe( const orgKeys$ = this.keyService.encryptedOrgKeys$(userId).pipe(
distinctUntilChanged(compareValues), // The upstream observable emits different objects with the same values distinctUntilChanged(compareValues), // The upstream observable emits different objects with the same values
); );
const securityState$ = this.securityStateService
.accountSecurityState$(userId)
.pipe(distinctUntilChanged(compareValues));
const client$ = combineLatest([ const client$ = combineLatest([
this.environmentService.getEnvironment$(userId), this.environmentService.getEnvironment$(userId),
@@ -171,51 +178,57 @@ export class DefaultSdkService implements SdkService {
kdfParams$, kdfParams$,
privateKey$, privateKey$,
userKey$, userKey$,
signingKey$,
orgKeys$, orgKeys$,
securityState$,
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(([env, account, kdfParams, privateKey, userKey, orgKeys]) => { switchMap(
// Create our own observable to be able to implement clean-up logic ([env, account, kdfParams, privateKey, userKey, signingKey, orgKeys, securityState]) => {
return new Observable<Rc<BitwardenClient>>((subscriber) => { // Create our own observable to be able to implement clean-up logic
const createAndInitializeClient = async () => { return new Observable<Rc<BitwardenClient>>((subscriber) => {
if (env == null || kdfParams == null || privateKey == null || userKey == null) { const createAndInitializeClient = async () => {
return undefined; if (env == null || kdfParams == null || privateKey == null || userKey == null) {
} return undefined;
}
const settings = this.toSettings(env); const settings = this.toSettings(env);
const client = await this.sdkClientFactory.createSdkClient( const client = await this.sdkClientFactory.createSdkClient(
new JsTokenProvider(this.apiService, userId), new JsTokenProvider(this.apiService, userId),
settings, settings,
); );
await this.initializeClient( await this.initializeClient(
userId, userId,
client, client,
account, account,
kdfParams, kdfParams,
privateKey, privateKey,
userKey, userKey,
orgKeys, signingKey,
); securityState,
orgKeys,
);
return client; return client;
}; };
let client: Rc<BitwardenClient> | undefined; let client: Rc<BitwardenClient> | undefined;
createAndInitializeClient() createAndInitializeClient()
.then((c) => { .then((c) => {
client = c === undefined ? undefined : new Rc(c); client = c === undefined ? undefined : new Rc(c);
subscriber.next(client); subscriber.next(client);
}) })
.catch((e) => { .catch((e) => {
subscriber.error(e); subscriber.error(e);
}); });
return () => client?.markForDisposal(); return () => client?.markForDisposal();
}); });
}), },
),
tap({ finalize: () => this.sdkClientCache.delete(userId) }), tap({ finalize: () => this.sdkClientCache.delete(userId) }),
shareReplay({ refCount: true, bufferSize: 1 }), shareReplay({ refCount: true, bufferSize: 1 }),
); );
@@ -231,6 +244,8 @@ export class DefaultSdkService implements SdkService {
kdfParams: KdfConfig, kdfParams: KdfConfig,
privateKey: EncryptedString, privateKey: EncryptedString,
userKey: UserKey, userKey: UserKey,
signingKey: WrappedSigningKey | null,
securityState: SignedSecurityState | null,
orgKeys: Record<OrganizationId, EncString>, orgKeys: Record<OrganizationId, EncString>,
) { ) {
await client.crypto().initialize_user_crypto({ await client.crypto().initialize_user_crypto({
@@ -248,8 +263,8 @@ export class DefaultSdkService implements SdkService {
}, },
}, },
privateKey, privateKey,
signingKey: undefined, signingKey: signingKey || undefined,
securityState: 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

@@ -11,6 +11,8 @@ import {
UserDecryptionOptions, UserDecryptionOptions,
UserDecryptionOptionsServiceAbstraction, UserDecryptionOptionsServiceAbstraction,
} from "@bitwarden/auth/common"; } 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. // 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, PBKDF2KdfConfig } from "@bitwarden/key-management"; import { KeyService, PBKDF2KdfConfig } from "@bitwarden/key-management";
@@ -72,6 +74,7 @@ describe("DefaultSyncService", () => {
let tokenService: MockProxy<TokenService>; let tokenService: MockProxy<TokenService>;
let authService: MockProxy<AuthService>; let authService: MockProxy<AuthService>;
let stateProvider: MockProxy<StateProvider>; let stateProvider: MockProxy<StateProvider>;
let securityStateService: MockProxy<SecurityStateService>;
let sut: DefaultSyncService; let sut: DefaultSyncService;
@@ -101,6 +104,7 @@ describe("DefaultSyncService", () => {
tokenService = mock(); tokenService = mock();
authService = mock(); authService = mock();
stateProvider = mock(); stateProvider = mock();
securityStateService = mock();
sut = new DefaultSyncService( sut = new DefaultSyncService(
masterPasswordAbstraction, masterPasswordAbstraction,
@@ -127,6 +131,7 @@ describe("DefaultSyncService", () => {
tokenService, tokenService,
authService, authService,
stateProvider, stateProvider,
securityStateService,
); );
}); });
@@ -155,6 +160,142 @@ describe("DefaultSyncService", () => {
stateProvider.getUser.mockReturnValue(mock()); stateProvider.getUser.mockReturnValue(mock());
}); });
it("sets the correct keys for a V1 user with old response model", async () => {
const v1Profile = {
id: user1,
key: "encryptedUserKey",
privateKey: "privateKey",
providers: [] as any[],
organizations: [] as any[],
providerOrganizations: [] as any[],
avatarColor: "#fff",
securityStamp: "stamp",
emailVerified: true,
verifyDevices: false,
premiumPersonally: false,
premiumFromOrganization: false,
usesKeyConnector: false,
};
apiService.getSync.mockResolvedValue(
new SyncResponse({
profile: v1Profile,
folders: [],
collections: [],
ciphers: [],
sends: [],
domains: [],
policies: [],
}),
);
await sut.fullSync(true);
expect(masterPasswordAbstraction.setMasterKeyEncryptedUserKey).toHaveBeenCalledWith(
new EncString("encryptedUserKey"),
user1,
);
expect(keyService.setPrivateKey).toHaveBeenCalledWith("privateKey", user1);
expect(keyService.setProviderKeys).toHaveBeenCalledWith([], user1);
expect(keyService.setOrgKeys).toHaveBeenCalledWith([], [], user1);
});
it("sets the correct keys for a V1 user", async () => {
const v1Profile = {
id: user1,
key: "encryptedUserKey",
privateKey: "privateKey",
providers: [] as any[],
organizations: [] as any[],
providerOrganizations: [] as any[],
avatarColor: "#fff",
securityStamp: "stamp",
emailVerified: true,
verifyDevices: false,
premiumPersonally: false,
premiumFromOrganization: false,
usesKeyConnector: false,
accountKeys: {
publicKeyEncryptionKeyPair: {
wrappedPrivateKey: "wrappedPrivateKey",
publicKey: "publicKey",
},
},
};
apiService.getSync.mockResolvedValue(
new SyncResponse({
profile: v1Profile,
folders: [],
collections: [],
ciphers: [],
sends: [],
domains: [],
policies: [],
}),
);
await sut.fullSync(true);
expect(masterPasswordAbstraction.setMasterKeyEncryptedUserKey).toHaveBeenCalledWith(
new EncString("encryptedUserKey"),
user1,
);
expect(keyService.setPrivateKey).toHaveBeenCalledWith("wrappedPrivateKey", user1);
expect(keyService.setProviderKeys).toHaveBeenCalledWith([], user1);
expect(keyService.setOrgKeys).toHaveBeenCalledWith([], [], user1);
});
it("sets the correct keys for a V2 user", async () => {
const v2Profile = {
id: user1,
key: "encryptedUserKey",
providers: [] as unknown[],
organizations: [] as unknown[],
providerOrganizations: [] as unknown[],
avatarColor: "#fff",
securityStamp: "stamp",
emailVerified: true,
verifyDevices: false,
premiumPersonally: false,
premiumFromOrganization: false,
usesKeyConnector: false,
privateKey: "wrappedPrivateKey",
accountKeys: {
publicKeyEncryptionKeyPair: {
wrappedPrivateKey: "wrappedPrivateKey",
publicKey: "publicKey",
signedPublicKey: "signedPublicKey",
},
signatureKeyPair: {
wrappedSigningKey: "wrappedSigningKey",
verifyingKey: "verifyingKey",
},
securityState: {
securityState: "securityState",
},
},
};
apiService.getSync.mockResolvedValue(
new SyncResponse({
profile: v2Profile,
folders: [],
collections: [],
ciphers: [],
sends: [],
domains: [],
policies: [],
}),
);
await sut.fullSync(true);
expect(masterPasswordAbstraction.setMasterKeyEncryptedUserKey).toHaveBeenCalledWith(
new EncString("encryptedUserKey"),
user1,
);
expect(keyService.setPrivateKey).toHaveBeenCalledWith("wrappedPrivateKey", user1);
expect(keyService.setUserSigningKey).toHaveBeenCalledWith("wrappedSigningKey", user1);
expect(securityStateService.setAccountSecurityState).toHaveBeenCalledWith(
"securityState",
user1,
);
expect(keyService.setProviderKeys).toHaveBeenCalledWith([], user1);
expect(keyService.setOrgKeys).toHaveBeenCalledWith([], [], user1);
});
it("does a token refresh when option missing from options", async () => { it("does a token refresh when option missing from options", async () => {
await sut.fullSync(true, { allowThrowOnError: false }); await sut.fullSync(true, { allowThrowOnError: false });

View File

@@ -10,6 +10,7 @@ import {
CollectionService, CollectionService,
} from "@bitwarden/admin-console/common"; } from "@bitwarden/admin-console/common";
// 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.
import { SecurityStateService } from "@bitwarden/common/key-management/security-state/abstractions/security-state.service";
// eslint-disable-next-line no-restricted-imports // eslint-disable-next-line no-restricted-imports
import { KeyService } from "@bitwarden/key-management"; import { KeyService } from "@bitwarden/key-management";
@@ -98,6 +99,7 @@ export class DefaultSyncService extends CoreSyncService {
tokenService: TokenService, tokenService: TokenService,
authService: AuthService, authService: AuthService,
stateProvider: StateProvider, stateProvider: StateProvider,
private securityStateService: SecurityStateService,
) { ) {
super( super(
tokenService, tokenService,
@@ -233,13 +235,34 @@ export class DefaultSyncService extends CoreSyncService {
if (response?.key) { if (response?.key) {
await this.masterPasswordService.setMasterKeyEncryptedUserKey(response.key, response.id); await this.masterPasswordService.setMasterKeyEncryptedUserKey(response.key, response.id);
} }
await this.keyService.setPrivateKey(response.privateKey, response.id);
// 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.keyService.setPrivateKey(
response.accountKeys.publicKeyEncryptionKeyPair.wrappedPrivateKey,
response.id,
);
if (response.accountKeys.signatureKeyPair !== null) {
// User is V2 user
await this.keyService.setUserSigningKey(
response.accountKeys.signatureKeyPair.wrappedSigningKey,
response.id,
);
await this.securityStateService.setAccountSecurityState(
response.accountKeys.securityState.securityState,
response.id,
);
}
} else {
await this.keyService.setPrivateKey(response.privateKey, response.id);
}
await this.keyService.setProviderKeys(response.providers, response.id); await this.keyService.setProviderKeys(response.providers, response.id);
await this.keyService.setOrgKeys( await this.keyService.setOrgKeys(
response.organizations, response.organizations,
response.providerOrganizations, response.providerOrganizations,
response.id, response.id,
); );
await this.avatarService.setSyncAvatarColor(response.id, response.avatarColor); await this.avatarService.setSyncAvatarColor(response.id, response.avatarColor);
await this.tokenService.setSecurityStamp(response.securityStamp, response.id); await this.tokenService.setSecurityStamp(response.securityStamp, response.id);
await this.accountService.setAccountEmailVerified(response.id, response.emailVerified); await this.accountService.setAccountEmailVerified(response.id, response.emailVerified);

View File

@@ -1,5 +1,6 @@
import { Opaque } from "type-fest"; import { Opaque } from "type-fest";
import { UnsignedPublicKey } from "../key-management/types";
import { SymmetricCryptoKey } from "../platform/models/domain/symmetric-crypto-key"; import { SymmetricCryptoKey } from "../platform/models/domain/symmetric-crypto-key";
// symmetric keys // symmetric keys
@@ -15,4 +16,4 @@ export type CipherKey = Opaque<SymmetricCryptoKey, "CipherKey">;
// asymmetric keys // asymmetric keys
export type UserPrivateKey = Opaque<Uint8Array, "UserPrivateKey">; export type UserPrivateKey = Opaque<Uint8Array, "UserPrivateKey">;
export type UserPublicKey = Opaque<Uint8Array, "UserPublicKey">; export type UserPublicKey = Opaque<UnsignedPublicKey, "UserPublicKey">;

View File

@@ -7,6 +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 { 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, UserId } from "@bitwarden/common/types/guid"; import { OrganizationId, UserId } from "@bitwarden/common/types/guid";
@@ -236,8 +237,10 @@ export abstract class KeyService {
*/ */
abstract getOrgKey(orgId: string): Promise<OrgKey | null>; abstract getOrgKey(orgId: string): Promise<OrgKey | null>;
/** /**
* Uses the org key to derive a new symmetric key for encrypting data * Makes a fresh attachment content encryption key and returns it along with a wrapped (encrypted) version of it.
* @param key The organization's symmetric key * @deprecated Do not use this for new code / new cryptographic designs.
* @param key The organization's symmetric key or the user's user key to wrap the attachment key with
* @returns The new attachment content encryption key and the wrapped version of it
*/ */
abstract makeDataEncKey<T extends UserKey | OrgKey>( abstract makeDataEncKey<T extends UserKey | OrgKey>(
key: T, key: T,
@@ -272,6 +275,14 @@ export abstract class KeyService {
* @param encPrivateKey An encrypted private key * @param encPrivateKey An encrypted private key
*/ */
abstract setPrivateKey(encPrivateKey: string, userId: UserId): Promise<void>; abstract setPrivateKey(encPrivateKey: string, userId: UserId): Promise<void>;
/**
* Sets the user's encrypted signing key in storage
* In contrast to the private key, the decrypted signing key
* is not stored in memory outside of the SDK.
* @param encryptedSigningKey An encrypted signing key
* @param userId The user id of the user to set the signing key for
*/
abstract setUserSigningKey(encryptedSigningKey: WrappedSigningKey, userId: UserId): Promise<void>;
/** /**
* Gets an observable stream of the given users decrypted private key, will emit null if the user * Gets an observable stream of the given users decrypted private key, will emit null if the user
@@ -416,7 +427,13 @@ export abstract class KeyService {
* *
* @throws If an invalid user id is passed in. * @throws If an invalid user id is passed in.
*/ */
abstract userPublicKey$(userId: UserId): Observable<UserPublicKey | null>; abstract userPublicKey$(userId: UserId): Observable<Uint8Array | null>;
/**
* Gets a users signing keys from local state.
* The observable will emit null, exactly if the local state returns null.
*/
abstract userSigningKey$(userId: UserId): Observable<WrappedSigningKey | null>;
/** /**
* Validates that a userkey is correct for a given user * Validates that a userkey is correct for a given user

View File

@@ -11,6 +11,7 @@ import {
} from "@bitwarden/common/key-management/crypto/models/enc-string"; } from "@bitwarden/common/key-management/crypto/models/enc-string";
import { FakeMasterPasswordService } from "@bitwarden/common/key-management/master-password/services/fake-master-password.service"; import { FakeMasterPasswordService } from "@bitwarden/common/key-management/master-password/services/fake-master-password.service";
import { PinServiceAbstraction } from "@bitwarden/common/key-management/pin/pin.service.abstraction"; import { PinServiceAbstraction } from "@bitwarden/common/key-management/pin/pin.service.abstraction";
import { UnsignedPublicKey, 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";
@@ -25,6 +26,7 @@ import {
USER_ENCRYPTED_PRIVATE_KEY, USER_ENCRYPTED_PRIVATE_KEY,
USER_EVER_HAD_USER_KEY, USER_EVER_HAD_USER_KEY,
USER_KEY, USER_KEY,
USER_KEY_ENCRYPTED_SIGNING_KEY,
} from "@bitwarden/common/platform/services/key-state/user-key.state"; } from "@bitwarden/common/platform/services/key-state/user-key.state";
import { UserKeyDefinition } from "@bitwarden/common/platform/state"; import { UserKeyDefinition } from "@bitwarden/common/platform/state";
import { import {
@@ -432,6 +434,7 @@ describe("keyService", () => {
USER_ENCRYPTED_ORGANIZATION_KEYS, USER_ENCRYPTED_ORGANIZATION_KEYS,
USER_ENCRYPTED_PROVIDER_KEYS, USER_ENCRYPTED_PROVIDER_KEYS,
USER_ENCRYPTED_PRIVATE_KEY, USER_ENCRYPTED_PRIVATE_KEY,
USER_KEY_ENCRYPTED_SIGNING_KEY,
USER_KEY, USER_KEY,
])("key removal", (key: UserKeyDefinition<unknown>) => { ])("key removal", (key: UserKeyDefinition<unknown>) => {
it(`clears ${key.key} for the specified user when specified`, async () => { it(`clears ${key.key} for the specified user when specified`, async () => {
@@ -540,6 +543,51 @@ describe("keyService", () => {
}); });
}); });
describe("userSigningKey$", () => {
it("returns the signing key when the user has a signing key set", async () => {
const fakeSigningKey = "" as WrappedSigningKey;
const fakeSigningKeyState = stateProvider.singleUser.getFake(
mockUserId,
USER_KEY_ENCRYPTED_SIGNING_KEY,
);
fakeSigningKeyState.nextState(fakeSigningKey);
const signingKey = await firstValueFrom(keyService.userSigningKey$(mockUserId));
expect(signingKey).toEqual(fakeSigningKey);
});
it("returns null when the user does not have a signing key set", async () => {
const signingKey = await firstValueFrom(keyService.userSigningKey$(mockUserId));
expect(signingKey).toBeFalsy();
});
});
describe("setUserSigningKey", () => {
it("throws if the signing key is null", async () => {
await expect(keyService.setUserSigningKey(null as any, mockUserId)).rejects.toThrow(
"No user signing key provided.",
);
});
it("throws if the userId is null", async () => {
await expect(
keyService.setUserSigningKey("" as WrappedSigningKey, null as unknown as UserId),
).rejects.toThrow("No userId provided.");
});
it("sets the signing key for the user", async () => {
const fakeSigningKey = "" as WrappedSigningKey;
const fakeSigningKeyState = stateProvider.singleUser.getFake(
mockUserId,
USER_KEY_ENCRYPTED_SIGNING_KEY,
);
fakeSigningKeyState.nextState(null);
await keyService.setUserSigningKey(fakeSigningKey, mockUserId);
expect(fakeSigningKeyState.nextMock).toHaveBeenCalledTimes(1);
expect(fakeSigningKeyState.nextMock).toHaveBeenCalledWith(fakeSigningKey);
});
});
describe("cipherDecryptionKeys$", () => { describe("cipherDecryptionKeys$", () => {
function fakePrivateKeyDecryption(encryptedPrivateKey: EncString, key: SymmetricCryptoKey) { function fakePrivateKeyDecryption(encryptedPrivateKey: EncString, key: SymmetricCryptoKey) {
const output = new Uint8Array(64); const output = new Uint8Array(64);
@@ -1132,12 +1180,12 @@ describe("keyService", () => {
keyService.userPrivateKey$ = jest.fn().mockReturnValue(new BehaviorSubject("private key")); keyService.userPrivateKey$ = jest.fn().mockReturnValue(new BehaviorSubject("private key"));
cryptoFunctionService.rsaExtractPublicKey.mockResolvedValue( cryptoFunctionService.rsaExtractPublicKey.mockResolvedValue(
Utils.fromUtf8ToArray("public key"), Utils.fromUtf8ToArray("public key") as UnsignedPublicKey,
); );
const key = await firstValueFrom(keyService.userEncryptionKeyPair$(mockUserId)); const key = await firstValueFrom(keyService.userEncryptionKeyPair$(mockUserId));
expect(key).toEqual({ expect(key).toEqual({
privateKey: "private key", privateKey: "private key",
publicKey: Utils.fromUtf8ToArray("public key"), publicKey: Utils.fromUtf8ToArray("public key") as UnsignedPublicKey,
}); });
}); });
}); });

View File

@@ -28,6 +28,7 @@ import {
} 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 { PinServiceAbstraction } from "@bitwarden/common/key-management/pin/pin.service.abstraction"; import { PinServiceAbstraction } from "@bitwarden/common/key-management/pin/pin.service.abstraction";
import { 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";
@@ -44,6 +45,7 @@ import {
USER_ENCRYPTED_PRIVATE_KEY, USER_ENCRYPTED_PRIVATE_KEY,
USER_EVER_HAD_USER_KEY, USER_EVER_HAD_USER_KEY,
USER_KEY, USER_KEY,
USER_KEY_ENCRYPTED_SIGNING_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";
@@ -398,8 +400,10 @@ export class DefaultKeyService implements KeyServiceAbstraction {
throw new Error("No key provided"); throw new Error("No key provided");
} }
const newSymKey = await this.keyGenerationService.createKey(512); // Content encryption key is AES256_CBC_HMAC
return this.buildProtectedSymmetricKey(key, newSymKey); const cek = await this.keyGenerationService.createKey(512);
const wrappedCek = await this.encryptService.wrapSymmetricKey(cek, key);
return [cek, wrappedCek];
} }
private async clearOrgKeys(userId: UserId): Promise<void> { private async clearOrgKeys(userId: UserId): Promise<void> {
@@ -505,6 +509,10 @@ export class DefaultKeyService implements KeyServiceAbstraction {
await this.stateProvider.setUserState(USER_ENCRYPTED_PRIVATE_KEY, null, userId); await this.stateProvider.setUserState(USER_ENCRYPTED_PRIVATE_KEY, null, userId);
} }
private async clearSigningKey(userId: UserId): Promise<void> {
await this.stateProvider.setUserState(USER_KEY_ENCRYPTED_SIGNING_KEY, null, userId);
}
async clearPinKeys(userId: UserId): Promise<void> { async clearPinKeys(userId: UserId): Promise<void> {
if (userId == null) { if (userId == null) {
throw new Error("UserId is required"); throw new Error("UserId is required");
@@ -537,6 +545,7 @@ export class DefaultKeyService implements KeyServiceAbstraction {
await this.clearOrgKeys(userId); await this.clearOrgKeys(userId);
await this.clearProviderKeys(userId); await this.clearProviderKeys(userId);
await this.clearKeyPair(userId); await this.clearKeyPair(userId);
await this.clearSigningKey(userId);
await this.clearPinKeys(userId); await this.clearPinKeys(userId);
await this.stateProvider.setUserState(USER_EVER_HAD_USER_KEY, null, userId); await this.stateProvider.setUserState(USER_EVER_HAD_USER_KEY, null, userId);
} }
@@ -758,6 +767,10 @@ export class DefaultKeyService implements KeyServiceAbstraction {
return phrase; return phrase;
} }
/**
* @deprecated
* This should only be used for wrapping the user key with a master key or stretched master key.
*/
private async buildProtectedSymmetricKey<T extends SymmetricCryptoKey>( private async buildProtectedSymmetricKey<T extends SymmetricCryptoKey>(
encryptionKey: SymmetricCryptoKey, encryptionKey: SymmetricCryptoKey,
newSymKey: SymmetricCryptoKey, newSymKey: SymmetricCryptoKey,
@@ -792,7 +805,7 @@ export class DefaultKeyService implements KeyServiceAbstraction {
return null; return null;
} }
return (await this.cryptoFunctionService.rsaExtractPublicKey(privateKey)) as UserPublicKey; return await this.cryptoFunctionService.rsaExtractPublicKey(privateKey);
} }
userPrivateKey$(userId: UserId): Observable<UserPrivateKey | null> { userPrivateKey$(userId: UserId): Observable<UserPrivateKey | null> {
@@ -808,7 +821,7 @@ export class DefaultKeyService implements KeyServiceAbstraction {
return null; return null;
} }
const publicKey = (await this.derivePublicKey(privateKey))!; const publicKey = (await this.derivePublicKey(privateKey))! as UserPublicKey;
return { privateKey, publicKey }; return { privateKey, publicKey };
}), }),
); );
@@ -905,6 +918,27 @@ export class DefaultKeyService implements KeyServiceAbstraction {
); );
} }
async setUserSigningKey(userSigningKey: WrappedSigningKey, userId: UserId): Promise<void> {
if (userSigningKey == null) {
throw new Error("No user signing key provided.");
}
if (userId == null) {
throw new Error("No userId provided.");
}
await this.stateProvider.setUserState(USER_KEY_ENCRYPTED_SIGNING_KEY, userSigningKey, userId);
}
userSigningKey$(userId: UserId): Observable<WrappedSigningKey | null> {
return this.stateProvider.getUser(userId, USER_KEY_ENCRYPTED_SIGNING_KEY).state$.pipe(
map((encryptedSigningKey) => {
if (encryptedSigningKey == null) {
return null;
}
return encryptedSigningKey as WrappedSigningKey;
}),
);
}
orgKeys$(userId: UserId): Observable<Record<OrganizationId, OrgKey> | null> { orgKeys$(userId: UserId): Observable<Record<OrganizationId, OrgKey> | null> {
return this.cipherDecryptionKeys$(userId).pipe(map((keys) => keys?.orgKeys ?? null)); return this.cipherDecryptionKeys$(userId).pipe(map((keys) => keys?.orgKeys ?? null));
} }

View File

@@ -3,6 +3,7 @@ import * as crypto from "crypto";
import * as forge from "node-forge"; import * as forge from "node-forge";
import { CryptoFunctionService } from "@bitwarden/common/key-management/crypto/abstractions/crypto-function.service"; import { CryptoFunctionService } from "@bitwarden/common/key-management/crypto/abstractions/crypto-function.service";
import { UnsignedPublicKey } from "@bitwarden/common/key-management/types";
import { EncryptionType } from "@bitwarden/common/platform/enums"; import { EncryptionType } from "@bitwarden/common/platform/enums";
import { Utils } from "@bitwarden/common/platform/misc/utils"; import { Utils } from "@bitwarden/common/platform/misc/utils";
import { import {
@@ -232,7 +233,7 @@ export class NodeCryptoFunctionService implements CryptoFunctionService {
return Promise.resolve(this.toUint8Buffer(decipher)); return Promise.resolve(this.toUint8Buffer(decipher));
} }
rsaExtractPublicKey(privateKey: Uint8Array): Promise<Uint8Array> { async rsaExtractPublicKey(privateKey: Uint8Array): Promise<UnsignedPublicKey> {
const privateKeyByteString = Utils.fromBufferToByteString(privateKey); const privateKeyByteString = Utils.fromBufferToByteString(privateKey);
const privateKeyAsn1 = forge.asn1.fromDer(privateKeyByteString); const privateKeyAsn1 = forge.asn1.fromDer(privateKeyByteString);
const forgePrivateKey: any = forge.pki.privateKeyFromAsn1(privateKeyAsn1); const forgePrivateKey: any = forge.pki.privateKeyFromAsn1(privateKeyAsn1);
@@ -240,11 +241,11 @@ export class NodeCryptoFunctionService implements CryptoFunctionService {
const publicKeyAsn1 = forge.pki.publicKeyToAsn1(forgePublicKey); const publicKeyAsn1 = forge.pki.publicKeyToAsn1(forgePublicKey);
const publicKeyByteString = forge.asn1.toDer(publicKeyAsn1).data; const publicKeyByteString = forge.asn1.toDer(publicKeyAsn1).data;
const publicKeyArray = Utils.fromByteStringToArray(publicKeyByteString); const publicKeyArray = Utils.fromByteStringToArray(publicKeyByteString);
return Promise.resolve(publicKeyArray); return publicKeyArray as UnsignedPublicKey;
} }
async rsaGenerateKeyPair(length: 1024 | 2048 | 4096): Promise<[Uint8Array, Uint8Array]> { async rsaGenerateKeyPair(length: 1024 | 2048 | 4096): Promise<[UnsignedPublicKey, Uint8Array]> {
return new Promise<[Uint8Array, Uint8Array]>((resolve, reject) => { return new Promise<[UnsignedPublicKey, Uint8Array]>((resolve, reject) => {
forge.pki.rsa.generateKeyPair( forge.pki.rsa.generateKeyPair(
{ {
bits: length, bits: length,
@@ -266,7 +267,7 @@ export class NodeCryptoFunctionService implements CryptoFunctionService {
const privateKeyByteString = forge.asn1.toDer(privateKeyPkcs8).getBytes(); const privateKeyByteString = forge.asn1.toDer(privateKeyPkcs8).getBytes();
const privateKey = Utils.fromByteStringToArray(privateKeyByteString); const privateKey = Utils.fromByteStringToArray(privateKeyByteString);
resolve([publicKey, privateKey]); resolve([publicKey as UnsignedPublicKey, privateKey]);
}, },
); );
}); });