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

[PM-21944] Split up userkey rotation v2 and add tests (#14900)

* Split up userkey rotation v2 and add tests

* Fix eslint

* Fix type errors

* Fix tests

* 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

* 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

---------

Co-authored-by: Thomas Avery <43214426+Thomas-Avery@users.noreply.github.com>
This commit is contained in:
Bernd Schoolmann
2025-06-10 15:57:47 +02:00
committed by GitHub
parent b5bddd0b06
commit 45605e9752
6 changed files with 1198 additions and 566 deletions

View File

@@ -1,25 +0,0 @@
import { OrganizationUserResetPasswordWithIdRequest } from "@bitwarden/admin-console/common";
import { WebauthnRotateCredentialRequest } from "@bitwarden/common/auth/models/request/webauthn-rotate-credential.request";
import { SendWithIdRequest } from "@bitwarden/common/tools/send/models/request/send-with-id.request";
import { CipherWithIdRequest } from "@bitwarden/common/vault/models/request/cipher-with-id.request";
import { FolderWithIdRequest } from "@bitwarden/common/vault/models/request/folder-with-id.request";
import { EmergencyAccessWithIdRequest } from "../../../auth/emergency-access/request/emergency-access-update.request";
export class UpdateKeyRequest {
masterPasswordHash: string;
key: string;
privateKey: string;
ciphers: CipherWithIdRequest[] = [];
folders: FolderWithIdRequest[] = [];
sends: SendWithIdRequest[] = [];
emergencyAccessKeys: EmergencyAccessWithIdRequest[] = [];
resetPasswordKeys: OrganizationUserResetPasswordWithIdRequest[] = [];
webauthnKeys: WebauthnRotateCredentialRequest[] = [];
constructor(masterPasswordHash: string, key: string, privateKey: string) {
this.masterPasswordHash = masterPasswordHash;
this.key = key;
this.privateKey = privateKey;
}
}

View File

@@ -3,17 +3,12 @@ import { inject, Injectable } from "@angular/core";
import { ApiService } from "@bitwarden/common/abstractions/api.service";
import { RotateUserAccountKeysRequest } from "./request/rotate-user-account-keys.request";
import { UpdateKeyRequest } from "./request/update-key.request";
@Injectable()
export class UserKeyRotationApiService {
readonly apiService = inject(ApiService);
postUserKeyUpdate(request: UpdateKeyRequest): Promise<any> {
return this.apiService.send("POST", "/accounts/key", request, true, false);
}
postUserKeyUpdateV2(request: RotateUserAccountKeysRequest): Promise<any> {
postUserKeyUpdate(request: RotateUserAccountKeysRequest): Promise<any> {
return this.apiService.send(
"POST",
"/accounts/key-management/rotate-user-account-keys",

View File

@@ -1,27 +1,27 @@
import { Injectable } from "@angular/core";
import { firstValueFrom } from "rxjs";
import { firstValueFrom, Observable } from "rxjs";
import { Account } from "@bitwarden/common/auth/abstractions/account.service";
import { UserVerificationService } from "@bitwarden/common/auth/abstractions/user-verification/user-verification.service.abstraction";
import { VerificationType } from "@bitwarden/common/auth/enums/verification-type";
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 { DeviceTrustServiceAbstraction } from "@bitwarden/common/key-management/device-trust/abstractions/device-trust.service.abstraction";
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 { HashPurpose } from "@bitwarden/common/platform/enums";
import { EncryptionType, HashPurpose } from "@bitwarden/common/platform/enums";
import { Utils } from "@bitwarden/common/platform/misc/utils";
import { EncString } from "@bitwarden/common/platform/models/domain/enc-string";
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";
import { UserKey } from "@bitwarden/common/types/key";
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
import { FolderService } from "@bitwarden/common/vault/abstractions/folder/folder.service.abstraction";
import { SyncService } from "@bitwarden/common/vault/abstractions/sync/sync.service.abstraction";
import { DialogService, ToastService } from "@bitwarden/components";
import { KeyService } from "@bitwarden/key-management";
import { KdfConfig, KdfConfigService, KeyService } from "@bitwarden/key-management";
import {
AccountRecoveryTrustComponent,
EmergencyAccessTrustComponent,
@@ -40,10 +40,16 @@ import { UnlockDataRequest } from "./request/unlock-data.request";
import { UserDataRequest } from "./request/userdata.request";
import { UserKeyRotationApiService } from "./user-key-rotation-api.service";
@Injectable()
type MasterPasswordAuthenticationAndUnlockData = {
masterPassword: string;
masterKeySalt: string;
masterKeyKdfConfig: KdfConfig;
masterPasswordHint: string;
};
@Injectable({ providedIn: "root" })
export class UserKeyRotationService {
constructor(
private userVerificationService: UserVerificationService,
private apiService: UserKeyRotationApiService,
private cipherService: CipherService,
private folderService: FolderService,
@@ -61,118 +67,345 @@ export class UserKeyRotationService {
private i18nService: I18nService,
private dialogService: DialogService,
private configService: ConfigService,
private cryptoFunctionService: CryptoFunctionService,
private kdfConfigService: KdfConfigService,
) {}
/**
* Creates a new user key and re-encrypts all required data with the it.
* @param oldMasterPassword: The current master password
* @param currentMasterPassword: The current master password
* @param newMasterPassword: The new master password
* @param user: The user account
* @param newMasterPasswordHint: The hint for the new master password
*/
async rotateUserKeyMasterPasswordAndEncryptedData(
oldMasterPassword: string,
currentMasterPassword: string,
newMasterPassword: string,
user: Account,
newMasterPasswordHint?: string,
): Promise<void> {
this.logService.info("[Userkey rotation] Starting user key rotation...");
if (!newMasterPassword) {
this.logService.info("[Userkey rotation] Invalid master password provided. Aborting!");
throw new Error("Invalid master password");
this.logService.info("[UserKey Rotation] Starting user key rotation...");
const upgradeToV2FeatureFlagEnabled = await this.configService.getFeatureFlag(
FeatureFlag.EnrollAeadOnKeyRotation,
);
// Make sure all conditions match - e.g. account state is up to date
await this.ensureIsAllowedToRotateUserKey();
// First, the provided organizations and emergency access users need to be verified;
// this is currently done by providing the user a manual confirmation dialog.
const { wasTrustDenied, trustedOrganizationPublicKeys, trustedEmergencyAccessUserPublicKeys } =
await this.verifyTrust(user);
if (wasTrustDenied) {
this.logService.info("[Userkey rotation] Trust was denied by user. Aborting!");
return;
}
// Read current cryptographic state / settings
const masterKeyKdfConfig: KdfConfig = (await this.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 this.firstValueFromOrThrow(
this.keyService.userKey$(user.id),
"User key",
))!;
const currentUserKeyWrappedPrivateKey = new EncString(
(await this.firstValueFromOrThrow(
this.keyService.userEncryptedPrivateKey$(user.id),
"User encrypted private key",
))!,
);
// 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;
}
// Assemble the key rotation request
const request = new RotateUserAccountKeysRequest(
await this.getAccountUnlockDataRequest(
user.id,
currentUserKey,
newUserKey,
{
masterPassword: newMasterPassword,
masterKeyKdfConfig,
masterKeySalt,
masterPasswordHint: newMasterPasswordHint,
} as MasterPasswordAuthenticationAndUnlockData,
trustedEmergencyAccessUserPublicKeys,
trustedOrganizationPublicKeys,
),
new AccountKeysRequest(wrappedPrivateKey.encryptedString!, publicKey),
await this.getAccountDataRequest(currentUserKey, newUserKey, user),
await this.makeServerMasterKeyAuthenticationHash(
currentMasterPassword,
masterKeyKdfConfig,
masterKeySalt,
),
);
this.logService.info("[Userkey rotation] Posting user key rotation request to server");
await this.apiService.postUserKeyUpdate(request);
this.logService.info("[Userkey rotation] Userkey rotation request posted to server");
this.toastService.showToast({
variant: "success",
title: this.i18nService.t("rotationCompletedTitle"),
message: this.i18nService.t("rotationCompletedDesc"),
timeout: 15000,
});
// temporary until userkey can be better verified
await this.vaultTimeoutService.logOut();
}
protected async ensureIsAllowedToRotateUserKey(): Promise<void> {
if ((await this.syncService.getLastSync()) === null) {
this.logService.info("[Userkey rotation] Client was never synced. Aborting!");
throw new Error(
"The local vault is de-synced and the keys cannot be rotated. Please log out and log back in to resolve this issue.",
);
}
}
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.
// 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);
// 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,
);
const newUserKeyWrappedPrivateKey = await this.encryptService.wrapDecapsulationKey(
privateKey,
newUserKey,
);
const publicKey = await this.cryptoFunctionService.rsaExtractPublicKey(privateKey);
return {
userKey: newUserKey,
asymmetricEncryptionKeys: {
wrappedPrivateKey: newUserKeyWrappedPrivateKey,
publicKey: Utils.fromBufferToB64(publicKey),
},
};
}
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");
}
protected async createMasterPasswordUnlockDataRequest(
userKey: UserKey,
newUnlockData: MasterPasswordAuthenticationAndUnlockData,
): Promise<MasterPasswordUnlockDataRequest> {
// Decryption via stretched-masterkey-wrapped-userkey
const newMasterKeyEncryptedUserKey = new EncString(
PureCrypto.encrypt_user_key_with_master_password(
userKey.toEncoded(),
newUnlockData.masterPassword,
newUnlockData.masterKeySalt,
newUnlockData.masterKeyKdfConfig.toSdkConfig(),
),
);
const newMasterKeyAuthenticationHash = await this.makeServerMasterKeyAuthenticationHash(
newUnlockData.masterPassword,
newUnlockData.masterKeyKdfConfig,
newUnlockData.masterKeySalt,
);
return new MasterPasswordUnlockDataRequest(
newUnlockData.masterKeyKdfConfig,
newUnlockData.masterKeySalt,
newMasterKeyAuthenticationHash,
newMasterKeyEncryptedUserKey.encryptedString!,
newUnlockData.masterPasswordHint,
);
}
protected async getAccountUnlockDataRequest(
userId: UserId,
currentUserKey: UserKey,
newUserKey: UserKey,
masterPasswordAuthenticationAndUnlockData: MasterPasswordAuthenticationAndUnlockData,
trustedEmergencyAccessGranteesPublicKeys: Uint8Array[],
trustedOrganizationPublicKeys: Uint8Array[],
): Promise<UnlockDataRequest> {
// To ensure access; all unlock methods need to be updated and provided the new user key.
// User unlock methods
let masterPasswordUnlockData: MasterPasswordUnlockDataRequest;
if (this.isUserWithMasterPassword(userId)) {
masterPasswordUnlockData = await this.createMasterPasswordUnlockDataRequest(
newUserKey,
masterPasswordAuthenticationAndUnlockData,
);
}
const passkeyUnlockData = await this.webauthnLoginAdminService.getRotatedData(
currentUserKey,
newUserKey,
userId,
);
const trustedDeviceUnlockData = await this.deviceTrustService.getRotatedData(
currentUserKey,
newUserKey,
userId,
);
// Unlock methods that share to a different user / group
const emergencyAccessUnlockData = await this.emergencyAccessService.getRotatedData(
newUserKey,
trustedEmergencyAccessGranteesPublicKeys,
userId,
);
const organizationAccountRecoveryUnlockData = (await this.resetPasswordService.getRotatedData(
newUserKey,
trustedOrganizationPublicKeys,
userId,
))!;
return new UnlockDataRequest(
masterPasswordUnlockData!,
emergencyAccessUnlockData,
organizationAccountRecoveryUnlockData,
passkeyUnlockData,
trustedDeviceUnlockData,
);
}
protected async verifyTrust(user: Account): Promise<{
wasTrustDenied: boolean;
trustedOrganizationPublicKeys: Uint8Array[];
trustedEmergencyAccessUserPublicKeys: Uint8Array[];
}> {
// Since currently the joined organizations and emergency access grantees are
// not signed, manual trust prompts are required, to verify that the server
// does not inject public keys here.
//
// Once signing is implemented, this is the place to also sign the keys and
// upload the signed trust claims.
//
// The flow works in 3 steps:
// 1. Prepare the user by showing them a dialog telling them they'll be asked
// to verify the trust of their organizations and emergency access users.
// 2. Show the user a dialog for each organization and ask them to verify the trust.
// 3. Show the user a dialog for each emergency access user and ask them to verify the trust.
this.logService.info("[Userkey rotation] Verifying trust...");
const emergencyAccessGrantees = await this.emergencyAccessService.getPublicKeys();
const orgs = await this.resetPasswordService.getPublicKeys(user.id);
if (orgs.length > 0 || emergencyAccessGrantees.length > 0) {
const organizations = await this.resetPasswordService.getPublicKeys(user.id);
if (organizations.length > 0 || emergencyAccessGrantees.length > 0) {
const trustInfoDialog = KeyRotationTrustInfoComponent.open(this.dialogService, {
numberOfEmergencyAccessUsers: emergencyAccessGrantees.length,
orgName: orgs.length > 0 ? orgs[0].orgName : undefined,
orgName: organizations.length > 0 ? organizations[0].orgName : undefined,
});
const result = await firstValueFrom(trustInfoDialog.closed);
if (!result) {
this.logService.info("[Userkey rotation] Trust info dialog closed. Aborting!");
return;
if (!(await firstValueFrom(trustInfoDialog.closed))) {
return {
wasTrustDenied: true,
trustedOrganizationPublicKeys: [],
trustedEmergencyAccessUserPublicKeys: [],
};
}
}
const {
masterKey: oldMasterKey,
email,
kdfConfig,
} = await this.userVerificationService.verifyUserByMasterPassword(
{
type: VerificationType.MasterPassword,
secret: oldMasterPassword,
},
user.id,
user.email,
);
const newMasterKey = await this.keyService.makeMasterKey(newMasterPassword, email, kdfConfig);
let userKeyBytes: Uint8Array;
if (await this.configService.getFeatureFlag(FeatureFlag.EnrollAeadOnKeyRotation)) {
userKeyBytes = PureCrypto.make_user_key_xchacha20_poly1305();
} else {
userKeyBytes = PureCrypto.make_user_key_aes256_cbc_hmac();
for (const organization of organizations) {
const dialogRef = AccountRecoveryTrustComponent.open(this.dialogService, {
name: organization.orgName,
orgId: organization.orgId,
publicKey: organization.publicKey,
});
if (!(await firstValueFrom(dialogRef.closed))) {
return {
wasTrustDenied: true,
trustedOrganizationPublicKeys: [],
trustedEmergencyAccessUserPublicKeys: [],
};
}
}
const newMasterKeyEncryptedUserKey = new EncString(
PureCrypto.encrypt_user_key_with_master_password(
userKeyBytes,
newMasterPassword,
email,
kdfConfig.toSdkConfig(),
),
);
const newUnencryptedUserKey = new SymmetricCryptoKey(userKeyBytes) as UserKey;
if (!newUnencryptedUserKey || !newMasterKeyEncryptedUserKey) {
this.logService.info("[Userkey rotation] User key could not be created. Aborting!");
throw new Error("User key could not be created");
for (const details of emergencyAccessGrantees) {
const dialogRef = EmergencyAccessTrustComponent.open(this.dialogService, {
name: details.name,
userId: details.granteeId,
publicKey: details.publicKey,
});
if (!(await firstValueFrom(dialogRef.closed))) {
return {
wasTrustDenied: true,
trustedOrganizationPublicKeys: [],
trustedEmergencyAccessUserPublicKeys: [],
};
}
}
const newMasterKeyAuthenticationHash = await this.keyService.hashMasterKey(
newMasterPassword,
newMasterKey,
HashPurpose.ServerAuthorization,
);
const masterPasswordUnlockData = new MasterPasswordUnlockDataRequest(
kdfConfig,
email,
newMasterKeyAuthenticationHash,
newMasterKeyEncryptedUserKey.encryptedString!,
newMasterPasswordHint,
this.logService.info(
"[Userkey rotation] Trust verified for all organizations and emergency access users",
);
return {
wasTrustDenied: false,
trustedOrganizationPublicKeys: organizations.map((d) => d.publicKey),
trustedEmergencyAccessUserPublicKeys: emergencyAccessGrantees.map((d) => d.publicKey),
};
}
const keyPair = await firstValueFrom(this.keyService.userEncryptionKeyPair$(user.id));
if (keyPair == null) {
this.logService.info("[Userkey rotation] Key pair is null. Aborting!");
throw new Error("Key pair is null");
}
const { privateKey, publicKey } = keyPair;
const accountKeysRequest = new AccountKeysRequest(
(
await this.encryptService.wrapDecapsulationKey(privateKey, newUnencryptedUserKey)
).encryptedString!,
Utils.fromBufferToB64(publicKey),
);
const originalUserKey = await firstValueFrom(this.keyService.userKey$(user.id));
if (originalUserKey == null) {
this.logService.info("[Userkey rotation] Userkey is null. Aborting!");
throw new Error("Userkey key is null");
}
protected async getAccountDataRequest(
originalUserKey: UserKey,
newUnencryptedUserKey: UserKey,
user: Account,
): Promise<UserDataRequest> {
// Account data is any data owned by the user; this is folders, ciphers (and their attachments), and sends.
// Currently, ciphers, folders and sends are directly encrypted with the user key. This means
// that they need to be re-encrypted and re-uploaded. In the future, content-encryption keys
// (such as cipher keys) will make it so only re-encrypted keys are required.
const rotatedCiphers = await this.cipherService.getRotatedData(
originalUserKey,
newUnencryptedUserKey,
@@ -192,111 +425,102 @@ export class UserKeyRotationService {
this.logService.info("[Userkey rotation] ciphers, folders, or sends are null. Aborting!");
throw new Error("ciphers, folders, or sends are null");
}
const accountDataRequest = new UserDataRequest(rotatedCiphers, rotatedFolders, rotatedSends);
return new UserDataRequest(rotatedCiphers, rotatedFolders, rotatedSends);
}
for (const details of emergencyAccessGrantees) {
this.logService.info("[Userkey rotation] Emergency access grantee: " + details.name);
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] Emergency access grantee fingerprint: " +
(await this.keyService.getFingerprint(details.granteeId, details.publicKey)).join("-"),
"[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.",
);
const dialogRef = EmergencyAccessTrustComponent.open(this.dialogService, {
name: details.name,
userId: details.granteeId,
publicKey: details.publicKey,
});
const result = await firstValueFrom(dialogRef.closed);
if (result === true) {
this.logService.info("[Userkey rotation] Emergency access grantee confirmed");
} else {
this.logService.info("[Userkey rotation] Emergency access grantee not confirmed");
return;
}
}
const trustedUserPublicKeys = emergencyAccessGrantees.map((d) => d.publicKey);
}
const emergencyAccessUnlockData = await this.emergencyAccessService.getRotatedData(
newUnencryptedUserKey,
trustedUserPublicKeys,
user.id,
);
for (const organization of orgs) {
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] Reset password organization: " + organization.orgName,
"[Userkey rotation] Existing userkey key is AES256-CBC-HMAC; upgrading to XChaCha20-Poly1305",
);
} else {
this.logService.info(
"[Userkey rotation] Trusted organization public key: " + organization.publicKey,
"[Userkey rotation] Existing userkey key is XChaCha20-Poly1305; no upgrade needed",
);
const fingerprint = await this.keyService.getFingerprint(
organization.orgId,
organization.publicKey,
);
this.logService.info(
"[Userkey rotation] Trusted organization fingerprint: " + fingerprint.join("-"),
);
const dialogRef = AccountRecoveryTrustComponent.open(this.dialogService, {
name: organization.orgName,
orgId: organization.orgId,
publicKey: organization.publicKey,
});
const result = await firstValueFrom(dialogRef.closed);
if (result === true) {
this.logService.info("[Userkey rotation] Organization trusted");
} else {
this.logService.info("[Userkey rotation] Organization not trusted");
return;
}
}
const trustedOrgPublicKeys = orgs.map((d) => d.publicKey);
// Note: Reset password keys request model has user verification
// properties, but the rotation endpoint uses its own MP hash.
const organizationAccountRecoveryUnlockData = (await this.resetPasswordService.getRotatedData(
newUnencryptedUserKey,
trustedOrgPublicKeys,
user.id,
))!;
const passkeyUnlockData = await this.webauthnLoginAdminService.getRotatedData(
originalUserKey,
newUnencryptedUserKey,
user.id,
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.
*/
protected isV1User(userKey: UserKey): boolean {
return userKey.inner().type === EncryptionType.AesCbc256_HmacSha256_B64;
}
protected isUserWithMasterPassword(id: UserId): boolean {
// Currently, key rotation can only be activated when the user has a master password.
return true;
}
protected async makeServerMasterKeyAuthenticationHash(
masterPassword: string,
masterKeyKdfConfig: KdfConfig,
masterKeySalt: string,
): Promise<string> {
const masterKey = await this.keyService.makeMasterKey(
masterPassword,
masterKeySalt,
masterKeyKdfConfig,
);
const trustedDeviceUnlockData = await this.deviceTrustService.getRotatedData(
originalUserKey,
newUnencryptedUserKey,
user.id,
return this.keyService.hashMasterKey(
masterPassword,
masterKey,
HashPurpose.ServerAuthorization,
);
}
const unlockDataRequest = new UnlockDataRequest(
masterPasswordUnlockData,
emergencyAccessUnlockData,
organizationAccountRecoveryUnlockData,
passkeyUnlockData,
trustedDeviceUnlockData,
);
const request = new RotateUserAccountKeysRequest(
unlockDataRequest,
accountKeysRequest,
accountDataRequest,
await this.keyService.hashMasterKey(oldMasterPassword, oldMasterKey),
);
this.logService.info("[Userkey rotation] Posting user key rotation request to server");
await this.apiService.postUserKeyUpdateV2(request);
this.logService.info("[Userkey rotation] Userkey rotation request posted to server");
this.toastService.showToast({
variant: "success",
title: this.i18nService.t("rotationCompletedTitle"),
message: this.i18nService.t("rotationCompletedDesc"),
timeout: 15000,
});
// temporary until userkey can be better verified
await this.vaultTimeoutService.logOut();
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;
}
}