mirror of
https://github.com/bitwarden/browser
synced 2025-12-10 05:13:29 +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:
@@ -100,6 +100,8 @@ import { InternalMasterPasswordServiceAbstraction } from "@bitwarden/common/key-
|
||||
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 { 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 {
|
||||
DefaultVaultTimeoutSettingsService,
|
||||
@@ -452,6 +454,7 @@ export default class MainBackground {
|
||||
taskService: TaskService;
|
||||
cipherEncryptionService: CipherEncryptionService;
|
||||
private restrictedItemTypesService: RestrictedItemTypesService;
|
||||
private securityStateService: SecurityStateService;
|
||||
|
||||
ipcContentScriptManagerService: IpcContentScriptManagerService;
|
||||
ipcService: IpcService;
|
||||
@@ -668,6 +671,8 @@ export default class MainBackground {
|
||||
logoutCallback,
|
||||
);
|
||||
|
||||
this.securityStateService = new DefaultSecurityStateService(this.stateProvider);
|
||||
|
||||
this.popupViewCacheBackgroundService = new PopupViewCacheBackgroundService(
|
||||
messageListener,
|
||||
this.globalStateProvider,
|
||||
@@ -830,6 +835,7 @@ export default class MainBackground {
|
||||
this.accountService,
|
||||
this.kdfConfigService,
|
||||
this.keyService,
|
||||
this.securityStateService,
|
||||
this.apiService,
|
||||
this.stateProvider,
|
||||
this.configService,
|
||||
@@ -999,7 +1005,6 @@ export default class MainBackground {
|
||||
this.avatarService = new AvatarService(this.apiService, this.stateProvider);
|
||||
|
||||
this.providerService = new ProviderService(this.stateProvider);
|
||||
|
||||
this.syncService = new DefaultSyncService(
|
||||
this.masterPasswordService,
|
||||
this.accountService,
|
||||
@@ -1025,6 +1030,7 @@ export default class MainBackground {
|
||||
this.tokenService,
|
||||
this.authService,
|
||||
this.stateProvider,
|
||||
this.securityStateService,
|
||||
);
|
||||
|
||||
this.syncServiceListener = new SyncServiceListener(
|
||||
|
||||
@@ -73,6 +73,8 @@ import { InternalMasterPasswordServiceAbstraction } from "@bitwarden/common/key-
|
||||
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 { 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 {
|
||||
DefaultVaultTimeoutService,
|
||||
DefaultVaultTimeoutSettingsService,
|
||||
@@ -305,6 +307,7 @@ export class ServiceContainer {
|
||||
cipherEncryptionService: CipherEncryptionService;
|
||||
restrictedItemTypesService: RestrictedItemTypesService;
|
||||
cliRestrictedItemTypesService: CliRestrictedItemTypesService;
|
||||
securityStateService: SecurityStateService;
|
||||
cipherArchiveService: CipherArchiveService;
|
||||
|
||||
constructor() {
|
||||
@@ -406,6 +409,8 @@ export class ServiceContainer {
|
||||
this.derivedStateProvider,
|
||||
);
|
||||
|
||||
this.securityStateService = new DefaultSecurityStateService(this.stateProvider);
|
||||
|
||||
this.environmentService = new DefaultEnvironmentService(
|
||||
this.stateProvider,
|
||||
this.accountService,
|
||||
@@ -612,6 +617,7 @@ export class ServiceContainer {
|
||||
this.accountService,
|
||||
this.kdfConfigService,
|
||||
this.keyService,
|
||||
this.securityStateService,
|
||||
this.apiService,
|
||||
this.stateProvider,
|
||||
this.configService,
|
||||
@@ -818,6 +824,7 @@ export class ServiceContainer {
|
||||
this.tokenService,
|
||||
this.authService,
|
||||
this.stateProvider,
|
||||
this.securityStateService,
|
||||
);
|
||||
|
||||
this.totpService = new TotpService(this.sdkService);
|
||||
|
||||
@@ -56,7 +56,9 @@ export class ProfileComponent implements OnInit, OnDestroy {
|
||||
this.profile = await this.apiService.getProfile();
|
||||
const userId = await firstValueFrom(getUserId(this.accountService.activeAccount$));
|
||||
this.fingerprintMaterial = userId;
|
||||
const publicKey = await firstValueFrom(this.keyService.userPublicKey$(userId));
|
||||
const publicKey = (await firstValueFrom(
|
||||
this.keyService.userPublicKey$(userId),
|
||||
)) as UserPublicKey;
|
||||
if (publicKey == null) {
|
||||
this.logService.error(
|
||||
"[ProfileComponent] No public key available for the user: " +
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -1,10 +1,70 @@
|
||||
export class AccountKeysRequest {
|
||||
// Other keys encrypted by the userkey
|
||||
userKeyEncryptedAccountPrivateKey: string;
|
||||
accountPublicKey: string;
|
||||
import { SecurityStateRequest } from "@bitwarden/common/key-management/security-state/request/security-state.request";
|
||||
import { WrappedPrivateKey } from "@bitwarden/common/key-management/types";
|
||||
import { SdkLoadService } from "@bitwarden/common/platform/abstractions/sdk/sdk-load.service";
|
||||
import { Utils } from "@bitwarden/common/platform/misc/utils";
|
||||
import { PureCrypto } from "@bitwarden/sdk-internal";
|
||||
|
||||
constructor(userKeyEncryptedAccountPrivateKey: string, accountPublicKey: string) {
|
||||
this.userKeyEncryptedAccountPrivateKey = userKeyEncryptedAccountPrivateKey;
|
||||
this.accountPublicKey = accountPublicKey;
|
||||
import { PublicKeyEncryptionKeyPairRequestModel } from "../model/public-key-encryption-key-pair-request.model";
|
||||
import { SignatureKeyPairRequestModel } from "../model/signature-key-pair-request-request.model";
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
};
|
||||
@@ -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,
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -4,6 +4,7 @@ import { BehaviorSubject } from "rxjs";
|
||||
import { OrganizationUserResetPasswordWithIdRequest } from "@bitwarden/admin-console/common";
|
||||
import { Account } from "@bitwarden/common/auth/abstractions/account.service";
|
||||
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 { EncryptService } from "@bitwarden/common/key-management/crypto/abstractions/encrypt.service";
|
||||
import {
|
||||
@@ -11,10 +12,22 @@ 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 { 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 { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.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 { SymmetricCryptoKey } from "@bitwarden/common/platform/models/domain/symmetric-crypto-key";
|
||||
import { SendWithIdRequest } from "@bitwarden/common/tools/send/models/request/send-with-id.request";
|
||||
@@ -33,13 +46,14 @@ import {
|
||||
PBKDF2KdfConfig,
|
||||
KdfConfigService,
|
||||
KdfConfig,
|
||||
KdfType,
|
||||
} from "@bitwarden/key-management";
|
||||
import {
|
||||
AccountRecoveryTrustComponent,
|
||||
EmergencyAccessTrustComponent,
|
||||
KeyRotationTrustInfoComponent,
|
||||
} 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 { 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 { 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 { UnlockDataRequest } from "./request/unlock-data.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 { UserKeyRotationService } from "./user-key-rotation.service";
|
||||
import {
|
||||
UserKeyRotationService,
|
||||
V1CryptographicStateParameters,
|
||||
V2CryptographicStateParameters,
|
||||
} from "./user-key-rotation.service";
|
||||
|
||||
const initialPromptedOpenTrue = jest.fn();
|
||||
initialPromptedOpenTrue.mockReturnValue({ closed: new BehaviorSubject(true) });
|
||||
@@ -120,6 +141,21 @@ function createMockWebauthn(id: string): any {
|
||||
} 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 {
|
||||
override rotateUserKeyMasterPasswordAndEncryptedData(
|
||||
currentMasterPassword: string,
|
||||
@@ -138,22 +174,17 @@ class TestUserKeyRotationService extends UserKeyRotationService {
|
||||
return super.ensureIsAllowedToRotateUserKey();
|
||||
}
|
||||
override getNewAccountKeysV1(
|
||||
currentUserKey: UserKey,
|
||||
currentUserKeyWrappedPrivateKey: EncString,
|
||||
): Promise<{
|
||||
userKey: UserKey;
|
||||
asymmetricEncryptionKeys: { wrappedPrivateKey: EncString; publicKey: string };
|
||||
}> {
|
||||
return super.getNewAccountKeysV1(currentUserKey, currentUserKeyWrappedPrivateKey);
|
||||
cryptographicStateParameters: V1CryptographicStateParameters,
|
||||
): Promise<V1UserCryptographicState> {
|
||||
return super.getNewAccountKeysV1(cryptographicStateParameters);
|
||||
}
|
||||
override getNewAccountKeysV2(
|
||||
currentUserKey: UserKey,
|
||||
currentUserKeyWrappedPrivateKey: EncString,
|
||||
): Promise<{
|
||||
userKey: UserKey;
|
||||
asymmetricEncryptionKeys: { wrappedPrivateKey: EncString; publicKey: string };
|
||||
}> {
|
||||
return super.getNewAccountKeysV2(currentUserKey, currentUserKeyWrappedPrivateKey);
|
||||
userId: UserId,
|
||||
kdfConfig: KdfConfig,
|
||||
email: string,
|
||||
cryptographicStateParameters: V1CryptographicStateParameters | V2CryptographicStateParameters,
|
||||
): Promise<V2UserCryptographicState> {
|
||||
return super.getNewAccountKeysV2(userId, kdfConfig, email, cryptographicStateParameters);
|
||||
}
|
||||
override createMasterPasswordUnlockDataRequest(
|
||||
userKey: UserKey,
|
||||
@@ -176,8 +207,8 @@ class TestUserKeyRotationService extends UserKeyRotationService {
|
||||
masterKeyKdfConfig: KdfConfig;
|
||||
masterPasswordHint: string;
|
||||
},
|
||||
trustedEmergencyAccessGranteesPublicKeys: Uint8Array[],
|
||||
trustedOrganizationPublicKeys: Uint8Array[],
|
||||
trustedEmergencyAccessGranteesPublicKeys: UnsignedPublicKey[],
|
||||
trustedOrganizationPublicKeys: UnsignedPublicKey[],
|
||||
): Promise<UnlockDataRequest> {
|
||||
return super.getAccountUnlockDataRequest(
|
||||
userId,
|
||||
@@ -190,8 +221,8 @@ class TestUserKeyRotationService extends UserKeyRotationService {
|
||||
}
|
||||
override verifyTrust(user: Account): Promise<{
|
||||
wasTrustDenied: boolean;
|
||||
trustedOrganizationPublicKeys: Uint8Array[];
|
||||
trustedEmergencyAccessUserPublicKeys: Uint8Array[];
|
||||
trustedOrganizationPublicKeys: UnsignedPublicKey[];
|
||||
trustedEmergencyAccessUserPublicKeys: UnsignedPublicKey[];
|
||||
}> {
|
||||
return super.verifyTrust(user);
|
||||
}
|
||||
@@ -202,14 +233,6 @@ class TestUserKeyRotationService extends UserKeyRotationService {
|
||||
): Promise<UserDataRequest> {
|
||||
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 {
|
||||
return super.isV1User(userKey);
|
||||
}
|
||||
@@ -227,6 +250,13 @@ class TestUserKeyRotationService extends UserKeyRotationService {
|
||||
masterKeySalt,
|
||||
);
|
||||
}
|
||||
override getCryptographicStateForUser(user: Account): Promise<{
|
||||
masterKeyKdfConfig: KdfConfig;
|
||||
masterKeySalt: string;
|
||||
cryptographicStateParameters: V1CryptographicStateParameters | V2CryptographicStateParameters;
|
||||
}> {
|
||||
return super.getCryptographicStateForUser(user);
|
||||
}
|
||||
}
|
||||
|
||||
describe("KeyRotationService", () => {
|
||||
@@ -251,6 +281,8 @@ describe("KeyRotationService", () => {
|
||||
let mockI18nService: MockProxy<I18nService>;
|
||||
let mockCryptoFunctionService: MockProxy<CryptoFunctionService>;
|
||||
let mockKdfConfigService: MockProxy<KdfConfigService>;
|
||||
let mockSdkClientFactory: MockProxy<SdkClientFactory>;
|
||||
let mockSecurityStateService: MockProxy<SecurityStateService>;
|
||||
|
||||
const mockUser = {
|
||||
id: "mockUserId" as UserId,
|
||||
@@ -261,6 +293,9 @@ describe("KeyRotationService", () => {
|
||||
|
||||
const mockTrustedPublicKeys = [Utils.fromUtf8ToArray("test-public-key")];
|
||||
|
||||
const mockMakeKeysForUserCryptoV2 = jest.fn();
|
||||
const mockGetV2RotatedAccountKeys = jest.fn();
|
||||
|
||||
beforeAll(() => {
|
||||
mockApiService = mock<UserKeyRotationApiService>();
|
||||
mockCipherService = mock<CipherService>();
|
||||
@@ -271,7 +306,7 @@ describe("KeyRotationService", () => {
|
||||
mockTrustedPublicKeys.map((key) => {
|
||||
return {
|
||||
publicKey: key,
|
||||
id: "mockId",
|
||||
id: "00000000-0000-0000-0000-000000000000" as UserId,
|
||||
granteeId: "mockGranteeId",
|
||||
name: "mockName",
|
||||
email: "mockEmail",
|
||||
@@ -306,6 +341,17 @@ describe("KeyRotationService", () => {
|
||||
mockDialogService = mock<DialogService>();
|
||||
mockCryptoFunctionService = mock<CryptoFunctionService>();
|
||||
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(
|
||||
mockApiService,
|
||||
@@ -327,6 +373,8 @@ describe("KeyRotationService", () => {
|
||||
mockConfigService,
|
||||
mockCryptoFunctionService,
|
||||
mockKdfConfigService,
|
||||
mockSdkClientFactory,
|
||||
mockSecurityStateService,
|
||||
);
|
||||
});
|
||||
|
||||
@@ -334,13 +382,16 @@ describe("KeyRotationService", () => {
|
||||
jest.clearAllMocks();
|
||||
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_xchacha20_poly1305").mockReturnValue(new Uint8Array(70));
|
||||
jest
|
||||
.spyOn(PureCrypto, "encrypt_user_key_with_master_password")
|
||||
.mockReturnValue("mockNewUserKey");
|
||||
Object.defineProperty(SdkLoadService, "Ready", {
|
||||
value: Promise.resolve(),
|
||||
configurable: true,
|
||||
});
|
||||
});
|
||||
|
||||
describe("rotateUserKeyAndEncryptedData", () => {
|
||||
describe("rotateUserKeyMasterPasswordAndEncryptedData", () => {
|
||||
let privateKey: BehaviorSubject<UserPrivateKey | null>;
|
||||
let keyPair: BehaviorSubject<{ privateKey: UserPrivateKey; publicKey: UserPublicKey }>;
|
||||
|
||||
@@ -438,6 +489,64 @@ describe("KeyRotationService", () => {
|
||||
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 () => {
|
||||
KeyRotationTrustInfoComponent.open = initialPromptedOpenTrue;
|
||||
AccountRecoveryTrustComponent.open = accountRecoveryTrustOpenTrusted;
|
||||
@@ -511,17 +620,17 @@ describe("KeyRotationService", () => {
|
||||
});
|
||||
|
||||
describe("getNewAccountKeysV1", () => {
|
||||
const currentUserKey = new SymmetricCryptoKey(new Uint8Array(64)) as UserKey;
|
||||
const mockEncryptedPrivateKey = new EncString(
|
||||
"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 = 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=",
|
||||
);
|
||||
const currentUserKey = TEST_VECTOR_USER_KEY_V1;
|
||||
const mockEncryptedPrivateKey = TEST_VECTOR_PRIVATE_KEY_V1 as WrappedPrivateKey;
|
||||
const mockNewEncryptedPrivateKey = TEST_VECTOR_PRIVATE_KEY_V1_ROTATED as WrappedPrivateKey;
|
||||
beforeAll(() => {
|
||||
mockEncryptService.unwrapDecapsulationKey.mockResolvedValue(new Uint8Array(200));
|
||||
mockEncryptService.wrapDecapsulationKey.mockResolvedValue(mockNewEncryptedPrivateKey);
|
||||
mockCryptoFunctionService.rsaExtractPublicKey.mockResolvedValue(new Uint8Array(400));
|
||||
mockEncryptService.wrapDecapsulationKey.mockResolvedValue(
|
||||
new EncString(mockNewEncryptedPrivateKey),
|
||||
);
|
||||
mockCryptoFunctionService.rsaExtractPublicKey.mockResolvedValue(
|
||||
new Uint8Array(400) as UnsignedPublicKey,
|
||||
);
|
||||
});
|
||||
|
||||
afterAll(() => {
|
||||
@@ -529,28 +638,110 @@ describe("KeyRotationService", () => {
|
||||
});
|
||||
|
||||
it("returns new account keys", async () => {
|
||||
const result = await keyRotationService.getNewAccountKeysV1(
|
||||
currentUserKey,
|
||||
mockEncryptedPrivateKey,
|
||||
);
|
||||
const result = await keyRotationService.getNewAccountKeysV1({
|
||||
version: 1,
|
||||
userKey: currentUserKey,
|
||||
publicKeyEncryptionKeyPair: {
|
||||
wrappedPrivateKey: mockEncryptedPrivateKey,
|
||||
publicKey: Utils.fromB64ToArray(TEST_VECTOR_PUBLIC_KEY_V1) as UnsignedPublicKey,
|
||||
},
|
||||
});
|
||||
expect(result).toEqual({
|
||||
userKey: expect.any(SymmetricCryptoKey),
|
||||
asymmetricEncryptionKeys: {
|
||||
publicKeyEncryptionKeyPair: {
|
||||
wrappedPrivateKey: mockNewEncryptedPrivateKey,
|
||||
publicKey: Utils.fromBufferToB64(new Uint8Array(400)),
|
||||
publicKey: new Uint8Array(400) as UserPublicKey,
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("getNewAccountKeysV2", () => {
|
||||
it("throws not supported", async () => {
|
||||
await expect(
|
||||
keyRotationService.getNewAccountKeysV2(
|
||||
new SymmetricCryptoKey(new Uint8Array(64)) as UserKey,
|
||||
null,
|
||||
),
|
||||
).rejects.toThrow("User encryption v2 upgrade is not supported yet");
|
||||
it("rotates a v2 user", async () => {
|
||||
mockGetV2RotatedAccountKeys.mockReturnValue({
|
||||
userKey: TEST_VECTOR_USER_KEY_V2.toBase64(),
|
||||
privateKey: TEST_VECTOR_PRIVATE_KEY_V2,
|
||||
publicKey: TEST_VECTOR_PUBLIC_KEY_V2,
|
||||
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: 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,
|
||||
);
|
||||
mockKeyService.hashMasterKey.mockResolvedValue("mockMasterPasswordHash");
|
||||
const newKey = new SymmetricCryptoKey(new Uint8Array(64)) as UserKey;
|
||||
const newKey = TEST_VECTOR_USER_KEY_V1;
|
||||
const userAccount = mockUser;
|
||||
const masterPasswordUnlockData =
|
||||
await keyRotationService.createMasterPasswordUnlockDataRequest(newKey, {
|
||||
@@ -572,13 +763,13 @@ describe("KeyRotationService", () => {
|
||||
expect(masterPasswordUnlockData).toEqual({
|
||||
masterKeyEncryptedUserKey: "mockNewUserKey",
|
||||
email: "mockEmail",
|
||||
kdfType: 0,
|
||||
kdfType: KdfType.PBKDF2_SHA256,
|
||||
kdfIterations: 600_000,
|
||||
masterKeyAuthenticationHash: "mockMasterPasswordHash",
|
||||
masterPasswordHint: "mockMasterPasswordHint",
|
||||
});
|
||||
expect(PureCrypto.encrypt_user_key_with_master_password).toHaveBeenCalledWith(
|
||||
new SymmetricCryptoKey(new Uint8Array(64)).toEncoded(),
|
||||
TEST_VECTOR_USER_KEY_V1.toEncoded(),
|
||||
"mockMasterPassword",
|
||||
userAccount.email,
|
||||
new PBKDF2KdfConfig(600_000).toSdkConfig(),
|
||||
@@ -637,8 +828,8 @@ describe("KeyRotationService", () => {
|
||||
masterKeyKdfConfig: new PBKDF2KdfConfig(600_000),
|
||||
masterPasswordHint: "mockMasterPasswordHint",
|
||||
},
|
||||
[new Uint8Array(1)], // emergency access public key
|
||||
[new Uint8Array(2)], // account recovery public key
|
||||
[new Uint8Array(1) as UnsignedPublicKey], // emergency access public key
|
||||
[new Uint8Array(2) as UnsignedPublicKey], // account recovery public key
|
||||
);
|
||||
expect(accountUnlockDataRequest.passkeyUnlockData).toEqual([
|
||||
{
|
||||
@@ -758,66 +949,29 @@ describe("KeyRotationService", () => {
|
||||
expect(wasTrustDenied).toBe(true);
|
||||
});
|
||||
|
||||
it("returns trusted keys if all dialogs are accepted", async () => {
|
||||
KeyRotationTrustInfoComponent.open = initialPromptedOpenTrue;
|
||||
EmergencyAccessTrustComponent.open = emergencyAccessTrustOpenTrusted;
|
||||
AccountRecoveryTrustComponent.open = accountRecoveryTrustOpenTrusted;
|
||||
mockEmergencyAccessService.getPublicKeys.mockResolvedValue([
|
||||
mockGranteeEmergencyAccessWithPublicKey,
|
||||
]);
|
||||
mockResetPasswordService.getPublicKeys.mockResolvedValue([
|
||||
mockOrganizationUserResetPasswordEntry,
|
||||
]);
|
||||
const {
|
||||
wasTrustDenied,
|
||||
trustedOrganizationPublicKeys: trustedOrgs,
|
||||
trustedEmergencyAccessUserPublicKeys: trustedEmergencyAccessUsers,
|
||||
} = await keyRotationService.verifyTrust(mockUser);
|
||||
expect(wasTrustDenied).toBe(false);
|
||||
expect(trustedEmergencyAccessUsers).toEqual([
|
||||
mockGranteeEmergencyAccessWithPublicKey.publicKey,
|
||||
]);
|
||||
expect(trustedOrgs).toEqual([mockOrganizationUserResetPasswordEntry.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,
|
||||
});
|
||||
});
|
||||
test.each([
|
||||
[[mockGranteeEmergencyAccessWithPublicKey], []],
|
||||
[[], [mockOrganizationUserResetPasswordEntry]],
|
||||
[[], []],
|
||||
[[mockGranteeEmergencyAccessWithPublicKey], [mockOrganizationUserResetPasswordEntry]],
|
||||
])(
|
||||
"returns trusted keys when dialogs are open and public keys are provided",
|
||||
async (emUsers, orgs) => {
|
||||
KeyRotationTrustInfoComponent.open = initialPromptedOpenTrue;
|
||||
EmergencyAccessTrustComponent.open = emergencyAccessTrustOpenTrusted;
|
||||
AccountRecoveryTrustComponent.open = accountRecoveryTrustOpenTrusted;
|
||||
mockEmergencyAccessService.getPublicKeys.mockResolvedValue(emUsers);
|
||||
mockResetPasswordService.getPublicKeys.mockResolvedValue(orgs);
|
||||
const {
|
||||
wasTrustDenied,
|
||||
trustedOrganizationPublicKeys: trustedOrgs,
|
||||
trustedEmergencyAccessUserPublicKeys: trustedEmergencyAccessUsers,
|
||||
} = await keyRotationService.verifyTrust(mockUser);
|
||||
expect(wasTrustDenied).toBe(false);
|
||||
expect(trustedEmergencyAccessUsers).toEqual(emUsers.map((e) => e.publicKey));
|
||||
expect(trustedOrgs).toEqual(orgs.map((o) => o.publicKey));
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
describe("getAccountDataRequest", () => {
|
||||
@@ -890,13 +1044,264 @@ describe("KeyRotationService", () => {
|
||||
});
|
||||
|
||||
describe("isV1UserKey", () => {
|
||||
const v1Key = new SymmetricCryptoKey(new Uint8Array(64));
|
||||
const v2Key = new SymmetricCryptoKey(new Uint8Array(70));
|
||||
const aes256CbcHmacV1UserKey = new SymmetricCryptoKey(new Uint8Array(64));
|
||||
const coseV2UserKey = new SymmetricCryptoKey(new Uint8Array(70));
|
||||
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", () => {
|
||||
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!",
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Injectable } from "@angular/core";
|
||||
import { firstValueFrom } from "rxjs";
|
||||
import { firstValueFrom, Observable } from "rxjs";
|
||||
|
||||
import { Account } from "@bitwarden/common/auth/abstractions/account.service";
|
||||
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 { 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 { 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 { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.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 { Utils } from "@bitwarden/common/platform/misc/utils";
|
||||
import { SymmetricCryptoKey } from "@bitwarden/common/platform/models/domain/symmetric-crypto-key";
|
||||
import { SendService } from "@bitwarden/common/tools/send/services/send.service.abstraction";
|
||||
import { UserId } from "@bitwarden/common/types/guid";
|
||||
@@ -28,7 +36,7 @@ import {
|
||||
EmergencyAccessTrustComponent,
|
||||
KeyRotationTrustInfoComponent,
|
||||
} 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 { 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 { UnlockDataRequest } from "./request/unlock-data.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";
|
||||
|
||||
type MasterPasswordAuthenticationAndUnlockData = {
|
||||
@@ -48,6 +61,19 @@ type MasterPasswordAuthenticationAndUnlockData = {
|
||||
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" })
|
||||
export class UserKeyRotationService {
|
||||
constructor(
|
||||
@@ -70,6 +96,8 @@ export class UserKeyRotationService {
|
||||
private configService: ConfigService,
|
||||
private cryptoFunctionService: CryptoFunctionService,
|
||||
private kdfConfigService: KdfConfigService,
|
||||
private sdkClientFactory: SdkClientFactory,
|
||||
private securityStateService: SecurityStateService,
|
||||
) {}
|
||||
|
||||
/**
|
||||
@@ -85,12 +113,15 @@ export class UserKeyRotationService {
|
||||
user: Account,
|
||||
newMasterPasswordHint?: string,
|
||||
): 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(
|
||||
FeatureFlag.EnrollAeadOnKeyRotation,
|
||||
);
|
||||
|
||||
this.logService.info("[UserKey Rotation] Starting user key rotation...");
|
||||
|
||||
// Make sure all conditions match - e.g. account state is up to date
|
||||
await this.ensureIsAllowedToRotateUserKey();
|
||||
|
||||
@@ -104,53 +135,26 @@ export class UserKeyRotationService {
|
||||
}
|
||||
|
||||
// Read current cryptographic state / settings
|
||||
const masterKeyKdfConfig: KdfConfig = (await firstValueFromOrThrow(
|
||||
this.kdfConfigService.getKdfConfig$(user.id),
|
||||
"KDF config",
|
||||
))!;
|
||||
// The masterkey salt used for deriving the masterkey always needs to be trimmed and lowercased.
|
||||
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",
|
||||
))!,
|
||||
);
|
||||
const {
|
||||
masterKeyKdfConfig,
|
||||
masterKeySalt,
|
||||
cryptographicStateParameters: currentCryptographicStateParameters,
|
||||
} = await this.getCryptographicStateForUser(user);
|
||||
|
||||
// Update account keys
|
||||
// This creates at least a new user key, and possibly upgrades user encryption formats
|
||||
let newUserKey: UserKey;
|
||||
let wrappedPrivateKey: EncString;
|
||||
let publicKey: string;
|
||||
if (upgradeToV2FeatureFlagEnabled) {
|
||||
this.logService.info("[Userkey rotation] Using v2 account keys");
|
||||
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;
|
||||
}
|
||||
// Get new set of keys for the account.
|
||||
const { userKey: newUserKey, accountKeysRequest } = await this.getRotatedAccountKeysFlagged(
|
||||
user.id,
|
||||
masterKeyKdfConfig,
|
||||
user.email,
|
||||
currentCryptographicStateParameters,
|
||||
upgradeToV2FeatureFlagEnabled,
|
||||
);
|
||||
|
||||
// Assemble the key rotation request
|
||||
const request = new RotateUserAccountKeysRequest(
|
||||
await this.getAccountUnlockDataRequest(
|
||||
user.id,
|
||||
currentUserKey,
|
||||
currentCryptographicStateParameters.userKey,
|
||||
newUserKey,
|
||||
{
|
||||
masterPassword: newMasterPassword,
|
||||
@@ -161,8 +165,12 @@ export class UserKeyRotationService {
|
||||
trustedEmergencyAccessUserPublicKeys,
|
||||
trustedOrganizationPublicKeys,
|
||||
),
|
||||
new AccountKeysRequest(wrappedPrivateKey.encryptedString!, publicKey),
|
||||
await this.getAccountDataRequest(currentUserKey, newUserKey, user),
|
||||
accountKeysRequest,
|
||||
await this.getAccountDataRequest(
|
||||
currentCryptographicStateParameters.userKey,
|
||||
newUserKey,
|
||||
user,
|
||||
),
|
||||
await this.makeServerMasterKeyAuthenticationHash(
|
||||
currentMasterPassword,
|
||||
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(
|
||||
currentUserKey: UserKey,
|
||||
currentUserKeyWrappedPrivateKey: EncString,
|
||||
): Promise<{
|
||||
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.
|
||||
cryptographicStateParameters: V1CryptographicStateParameters,
|
||||
): Promise<V1UserCryptographicState> {
|
||||
// Account key rotation creates a new user key. 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
|
||||
// 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
|
||||
// Rotation of the private key is not supported yet
|
||||
const privateKey = await this.encryptService.unwrapDecapsulationKey(
|
||||
currentUserKeyWrappedPrivateKey,
|
||||
currentUserKey,
|
||||
new EncString(cryptographicStateParameters.publicKeyEncryptionKeyPair.wrappedPrivateKey),
|
||||
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,
|
||||
newUserKey,
|
||||
);
|
||||
const publicKey = await this.cryptoFunctionService.rsaExtractPublicKey(privateKey);
|
||||
)) as UnsignedPublicKey;
|
||||
|
||||
return {
|
||||
userKey: newUserKey,
|
||||
asymmetricEncryptionKeys: {
|
||||
publicKeyEncryptionKeyPair: {
|
||||
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(
|
||||
currentUserKey: UserKey,
|
||||
currentUserKeyWrappedPrivateKey: EncString,
|
||||
): Promise<{
|
||||
userKey: UserKey;
|
||||
asymmetricEncryptionKeys: {
|
||||
wrappedPrivateKey: EncString;
|
||||
publicKey: string;
|
||||
};
|
||||
}> {
|
||||
throw new Error("User encryption v2 upgrade is not supported yet");
|
||||
userId: UserId,
|
||||
masterKeyKdfConfig: KdfConfig,
|
||||
masterKeySalt: string,
|
||||
cryptographicStateParameters: V1CryptographicStateParameters | V2CryptographicStateParameters,
|
||||
): Promise<V2UserCryptographicState> {
|
||||
if (cryptographicStateParameters.version === 1) {
|
||||
return this.upgradeV1UserToV2UserAccountKeys(
|
||||
userId,
|
||||
masterKeyKdfConfig,
|
||||
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(
|
||||
userKey: UserKey,
|
||||
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(
|
||||
userId: UserId,
|
||||
currentUserKey: UserKey,
|
||||
newUserKey: UserKey,
|
||||
masterPasswordAuthenticationAndUnlockData: MasterPasswordAuthenticationAndUnlockData,
|
||||
trustedEmergencyAccessGranteesPublicKeys: Uint8Array[],
|
||||
trustedOrganizationPublicKeys: Uint8Array[],
|
||||
trustedEmergencyAccessGranteesPublicKeys: UnsignedPublicKey[],
|
||||
trustedOrganizationPublicKeys: UnsignedPublicKey[],
|
||||
): Promise<UnlockDataRequest> {
|
||||
// To ensure access; all unlock methods need to be updated and provided the new user key.
|
||||
// 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<{
|
||||
wasTrustDenied: boolean;
|
||||
trustedOrganizationPublicKeys: Uint8Array[];
|
||||
trustedEmergencyAccessUserPublicKeys: Uint8Array[];
|
||||
trustedOrganizationPublicKeys: UnsignedPublicKey[];
|
||||
trustedEmergencyAccessUserPublicKeys: UnsignedPublicKey[];
|
||||
}> {
|
||||
// Since currently the joined organizations and emergency access grantees are
|
||||
// not signed, manual trust prompts are required, to verify that the server
|
||||
@@ -392,11 +505,16 @@ export class UserKeyRotationService {
|
||||
);
|
||||
return {
|
||||
wasTrustDenied: false,
|
||||
trustedOrganizationPublicKeys: organizations.map((d) => d.publicKey),
|
||||
trustedEmergencyAccessUserPublicKeys: emergencyAccessGrantees.map((d) => d.publicKey),
|
||||
trustedOrganizationPublicKeys: organizations.map((d) => d.publicKey as UnsignedPublicKey),
|
||||
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(
|
||||
originalUserKey: UserKey,
|
||||
newUnencryptedUserKey: UserKey,
|
||||
@@ -429,64 +547,6 @@ export class UserKeyRotationService {
|
||||
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 V2 user has a signing key, and uses XChaCha20-Poly1305.
|
||||
@@ -516,4 +576,111 @@ export class UserKeyRotationService {
|
||||
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;
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user