mirror of
https://github.com/bitwarden/browser
synced 2026-02-06 19:53:59 +00:00
Merge branch 'km/move-clientkeyhalf-to-platform-impl' into km/simplify-linux-biometrics
This commit is contained in:
@@ -437,7 +437,7 @@ export default class MainBackground {
|
||||
|
||||
constructor() {
|
||||
// Services
|
||||
const lockedCallback = async (userId?: string) => {
|
||||
const lockedCallback = async (userId: UserId) => {
|
||||
await this.refreshBadge();
|
||||
await this.refreshMenu(true);
|
||||
if (this.systemService != null) {
|
||||
|
||||
@@ -31,7 +31,11 @@
|
||||
<a bitMenuItem (click)="clone()" *ngIf="canClone$ | async">
|
||||
{{ "clone" | i18n }}
|
||||
</a>
|
||||
<a bitMenuItem *ngIf="hasOrganizations" (click)="conditionallyNavigateToAssignCollections()">
|
||||
<a
|
||||
bitMenuItem
|
||||
*ngIf="canAssignCollections$ | async"
|
||||
(click)="conditionallyNavigateToAssignCollections()"
|
||||
>
|
||||
{{ "assignToCollections" | i18n }}
|
||||
</a>
|
||||
</ng-container>
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
// FIXME: Update this file to be type safe and remove this and next line
|
||||
// @ts-strict-ignore
|
||||
import { CommonModule } from "@angular/common";
|
||||
import { booleanAttribute, Component, Input, OnInit } from "@angular/core";
|
||||
import { booleanAttribute, Component, Input } from "@angular/core";
|
||||
import { Router, RouterModule } from "@angular/router";
|
||||
import { BehaviorSubject, firstValueFrom, map, switchMap } from "rxjs";
|
||||
import { BehaviorSubject, combineLatest, firstValueFrom, map, switchMap } from "rxjs";
|
||||
import { filter } from "rxjs/operators";
|
||||
|
||||
import { CollectionService } from "@bitwarden/admin-console/common";
|
||||
import { JslibModule } from "@bitwarden/angular/jslib.module";
|
||||
import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction";
|
||||
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||
@@ -32,7 +33,7 @@ import { AddEditQueryParams } from "../add-edit/add-edit-v2.component";
|
||||
templateUrl: "./item-more-options.component.html",
|
||||
imports: [ItemModule, IconButtonModule, MenuModule, CommonModule, JslibModule, RouterModule],
|
||||
})
|
||||
export class ItemMoreOptionsComponent implements OnInit {
|
||||
export class ItemMoreOptionsComponent {
|
||||
private _cipher$ = new BehaviorSubject<CipherView>(undefined);
|
||||
|
||||
@Input({
|
||||
@@ -71,8 +72,21 @@ export class ItemMoreOptionsComponent implements OnInit {
|
||||
switchMap((c) => this.cipherAuthorizationService.canCloneCipher$(c)),
|
||||
);
|
||||
|
||||
/** Boolean dependent on the current user having access to an organization */
|
||||
protected hasOrganizations = false;
|
||||
/** Observable Boolean dependent on the current user having access to an organization and editable collections */
|
||||
protected canAssignCollections$ = this.accountService.activeAccount$.pipe(
|
||||
getUserId,
|
||||
switchMap((userId) => {
|
||||
return combineLatest([
|
||||
this.organizationService.hasOrganizations(userId),
|
||||
this.collectionService.decryptedCollections$,
|
||||
]).pipe(
|
||||
map(([hasOrgs, collections]) => {
|
||||
const canEditCollections = collections.some((c) => !c.readOnly);
|
||||
return hasOrgs && canEditCollections;
|
||||
}),
|
||||
);
|
||||
}),
|
||||
);
|
||||
|
||||
constructor(
|
||||
private cipherService: CipherService,
|
||||
@@ -85,13 +99,9 @@ export class ItemMoreOptionsComponent implements OnInit {
|
||||
private accountService: AccountService,
|
||||
private organizationService: OrganizationService,
|
||||
private cipherAuthorizationService: CipherAuthorizationService,
|
||||
private collectionService: CollectionService,
|
||||
) {}
|
||||
|
||||
async ngOnInit(): Promise<void> {
|
||||
const userId = await firstValueFrom(getUserId(this.accountService.activeAccount$));
|
||||
this.hasOrganizations = await firstValueFrom(this.organizationService.hasOrganizations(userId));
|
||||
}
|
||||
|
||||
get canEdit() {
|
||||
return this.cipher.edit;
|
||||
}
|
||||
|
||||
@@ -715,8 +715,8 @@ export class ServiceContainer {
|
||||
|
||||
this.folderApiService = new FolderApiService(this.folderService, this.apiService);
|
||||
|
||||
const lockedCallback = async (userId?: string) =>
|
||||
await this.keyService.clearStoredUserKey(KeySuffixOptions.Auto);
|
||||
const lockedCallback = async (userId: UserId) =>
|
||||
await this.keyService.clearStoredUserKey(KeySuffixOptions.Auto, userId);
|
||||
|
||||
this.userVerificationApiService = new UserVerificationApiService(this.apiService);
|
||||
|
||||
|
||||
@@ -55,7 +55,7 @@ export class ElectronKeyService extends DefaultKeyService {
|
||||
return super.hasUserKeyStored(keySuffix, userId);
|
||||
}
|
||||
|
||||
override async clearStoredUserKey(keySuffix: KeySuffixOptions, userId?: UserId): Promise<void> {
|
||||
override async clearStoredUserKey(keySuffix: KeySuffixOptions, userId: UserId): Promise<void> {
|
||||
await super.clearStoredUserKey(keySuffix, userId);
|
||||
}
|
||||
|
||||
|
||||
@@ -788,7 +788,13 @@
|
||||
<button
|
||||
type="button"
|
||||
(click)="share()"
|
||||
*ngIf="editMode && cipher && !cipher.organizationId && !cloneMode"
|
||||
*ngIf="
|
||||
editMode &&
|
||||
cipher &&
|
||||
!cipher.organizationId &&
|
||||
!cloneMode &&
|
||||
writeableCollections.length > 0
|
||||
"
|
||||
>
|
||||
{{ "move" | i18n }}
|
||||
</button>
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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",
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -22,7 +22,6 @@ import { SmLandingApiService } from "./sm-landing-api.service";
|
||||
|
||||
@Component({
|
||||
selector: "app-request-sm-access",
|
||||
standalone: true,
|
||||
templateUrl: "request-sm-access.component.html",
|
||||
imports: [SharedModule, SearchModule, NoItemsModule, HeaderModule, OssModule],
|
||||
})
|
||||
|
||||
@@ -14,7 +14,6 @@ import { SharedModule } from "../../shared/shared.module";
|
||||
|
||||
@Component({
|
||||
selector: "app-sm-landing",
|
||||
standalone: true,
|
||||
imports: [SharedModule, SearchModule, NoItemsModule, HeaderModule],
|
||||
templateUrl: "sm-landing.component.html",
|
||||
})
|
||||
|
||||
@@ -298,8 +298,11 @@ export class VaultItemsComponent {
|
||||
|
||||
protected canAssignCollections(cipher: CipherView) {
|
||||
const organization = this.allOrganizations.find((o) => o.id === cipher.organizationId);
|
||||
const editableCollections = this.allCollections.filter((c) => !c.readOnly);
|
||||
|
||||
return (
|
||||
(organization?.canEditAllCiphers && this.viewingOrgVault) || cipher.canAssignToCollections
|
||||
(organization?.canEditAllCiphers && this.viewingOrgVault) ||
|
||||
(cipher.canAssignToCollections && editableCollections.length > 0)
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
// @ts-strict-ignore
|
||||
import { Observable } from "rxjs";
|
||||
|
||||
import { DeviceKeysUpdateRequest } from "@bitwarden/common/auth/models/request/update-devices-trust.request";
|
||||
import { OtherDeviceKeysUpdateRequest } from "@bitwarden/common/auth/models/request/update-devices-trust.request";
|
||||
|
||||
import { DeviceResponse } from "../../../auth/abstractions/devices/responses/device.response";
|
||||
import { EncString } from "../../../platform/models/domain/enc-string";
|
||||
@@ -61,5 +61,5 @@ export abstract class DeviceTrustServiceAbstraction {
|
||||
oldUserKey: UserKey,
|
||||
newUserKey: UserKey,
|
||||
userId: UserId,
|
||||
) => Promise<DeviceKeysUpdateRequest[]>;
|
||||
) => Promise<OtherDeviceKeysUpdateRequest[]>;
|
||||
}
|
||||
|
||||
@@ -200,7 +200,7 @@ export class DeviceTrustService implements DeviceTrustServiceAbstraction {
|
||||
oldUserKey: UserKey,
|
||||
newUserKey: UserKey,
|
||||
userId: UserId,
|
||||
): Promise<DeviceKeysUpdateRequest[]> {
|
||||
): Promise<OtherDeviceKeysUpdateRequest[]> {
|
||||
if (!userId) {
|
||||
throw new Error("UserId is required. Cannot get rotated data.");
|
||||
}
|
||||
|
||||
@@ -417,16 +417,12 @@ describe("VaultTimeoutService", () => {
|
||||
expect(stateEventRunnerService.handleEvent).toHaveBeenCalledWith("lock", "user1");
|
||||
});
|
||||
|
||||
it("should call locked callback if no user passed into lock", async () => {
|
||||
it("should call locked callback with the locking user if no userID is passed in.", async () => {
|
||||
setupLock();
|
||||
|
||||
await vaultTimeoutService.lock();
|
||||
|
||||
// Currently these pass `undefined` (or what they were given) as the userId back
|
||||
// but we could change this to give the user that was locked (active) to these methods
|
||||
// so they don't have to get it their own way, but that is a behavioral change that needs
|
||||
// to be tested.
|
||||
expect(lockedCallback).toHaveBeenCalledWith(undefined);
|
||||
expect(lockedCallback).toHaveBeenCalledWith("user1");
|
||||
});
|
||||
|
||||
it("should call state event runner with user passed into lock", async () => {
|
||||
|
||||
@@ -49,7 +49,7 @@ export class VaultTimeoutService implements VaultTimeoutServiceAbstraction {
|
||||
private taskSchedulerService: TaskSchedulerService,
|
||||
protected logService: LogService,
|
||||
private biometricService: BiometricsService,
|
||||
private lockedCallback: (userId?: string) => Promise<void> = null,
|
||||
private lockedCallback: (userId: UserId) => Promise<void> = null,
|
||||
private loggedOutCallback: (
|
||||
logoutReason: LogoutReason,
|
||||
userId?: string,
|
||||
@@ -166,7 +166,7 @@ export class VaultTimeoutService implements VaultTimeoutServiceAbstraction {
|
||||
this.messagingService.send("locked", { userId: lockingUserId });
|
||||
|
||||
if (this.lockedCallback != null) {
|
||||
await this.lockedCallback(userId);
|
||||
await this.lockedCallback(lockingUserId);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -167,8 +167,9 @@ export abstract class KeyService {
|
||||
* Clears the user's stored version of the user key
|
||||
* @param keySuffix The desired version of the key to clear
|
||||
* @param userId The desired user
|
||||
* @throws Error when userId is null or undefined.
|
||||
*/
|
||||
abstract clearStoredUserKey(keySuffix: KeySuffixOptions, userId?: string): Promise<void>;
|
||||
abstract clearStoredUserKey(keySuffix: KeySuffixOptions, userId: string): Promise<void>;
|
||||
/**
|
||||
* Stores the master key encrypted user key
|
||||
* @throws Error when userId is null and there is no active user.
|
||||
|
||||
@@ -410,6 +410,45 @@ describe("keyService", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("clearStoredUserKey", () => {
|
||||
describe("input validation", () => {
|
||||
const invalidUserIdTestCases = [
|
||||
{ keySuffix: KeySuffixOptions.Auto, userId: null as unknown as UserId },
|
||||
{ keySuffix: KeySuffixOptions.Auto, userId: undefined as unknown as UserId },
|
||||
{ keySuffix: KeySuffixOptions.Pin, userId: null as unknown as UserId },
|
||||
{ keySuffix: KeySuffixOptions.Pin, userId: undefined as unknown as UserId },
|
||||
];
|
||||
test.each(invalidUserIdTestCases)(
|
||||
"throws when keySuffix is $keySuffix and userId is $userId",
|
||||
async ({ keySuffix, userId }) => {
|
||||
await expect(keyService.clearStoredUserKey(keySuffix, userId)).rejects.toThrow(
|
||||
"UserId is required",
|
||||
);
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
describe("with Auto key suffix", () => {
|
||||
it("UserKeyAutoUnlock is cleared and pin keys are not cleared", async () => {
|
||||
await keyService.clearStoredUserKey(KeySuffixOptions.Auto, mockUserId);
|
||||
|
||||
expect(stateService.setUserKeyAutoUnlock).toHaveBeenCalledWith(null, {
|
||||
userId: mockUserId,
|
||||
});
|
||||
expect(pinService.clearPinKeyEncryptedUserKeyEphemeral).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe("with PIN key suffix", () => {
|
||||
it("pin keys are cleared and user key auto unlock not", async () => {
|
||||
await keyService.clearStoredUserKey(KeySuffixOptions.Pin, mockUserId);
|
||||
|
||||
expect(stateService.setUserKeyAutoUnlock).not.toHaveBeenCalled();
|
||||
expect(pinService.clearPinKeyEncryptedUserKeyEphemeral).toHaveBeenCalledWith(mockUserId);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("clearKeys", () => {
|
||||
test.each([null as unknown as UserId, undefined as unknown as UserId])(
|
||||
"throws when the provided userId is %s",
|
||||
|
||||
@@ -250,16 +250,16 @@ export class DefaultKeyService implements KeyServiceAbstraction {
|
||||
await this.clearAllStoredUserKeys(userId);
|
||||
}
|
||||
|
||||
async clearStoredUserKey(keySuffix: KeySuffixOptions, userId?: UserId): Promise<void> {
|
||||
if (keySuffix === KeySuffixOptions.Auto) {
|
||||
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
|
||||
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
||||
this.stateService.setUserKeyAutoUnlock(null, { userId: userId });
|
||||
async clearStoredUserKey(keySuffix: KeySuffixOptions, userId: UserId): Promise<void> {
|
||||
if (userId == null) {
|
||||
throw new Error("UserId is required");
|
||||
}
|
||||
if (keySuffix === KeySuffixOptions.Pin && userId != null) {
|
||||
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
|
||||
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
||||
this.pinService.clearPinKeyEncryptedUserKeyEphemeral(userId);
|
||||
|
||||
if (keySuffix === KeySuffixOptions.Auto) {
|
||||
await this.stateService.setUserKeyAutoUnlock(null, { userId: userId });
|
||||
}
|
||||
if (keySuffix === KeySuffixOptions.Pin) {
|
||||
await this.pinService.clearPinKeyEncryptedUserKeyEphemeral(userId);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user