mirror of
https://github.com/bitwarden/browser
synced 2025-12-13 06:43:35 +00:00
[PM-14445] TS strict for Key Management, Keys and Lock component (#13121)
* PM-14445: TS strict for Key Management Biometrics * formatting * callbacks not null expectations * state nullability expectations updates * unit tests fix * secure channel naming, explicit null check on messageId * KM-14445: TS strict for Key Management, Keys and Lock component * conflicts resolution, new strict check failures * null simplifications * migrate legacy encryption when no active user throw error instead of hiding it * throw instead of return
This commit is contained in:
@@ -1,5 +1,3 @@
|
||||
// FIXME: Update this file to be type safe and remove this and next line
|
||||
// @ts-strict-ignore
|
||||
import { inject } from "@angular/core";
|
||||
import { combineLatest, defer, firstValueFrom, map, Observable } from "rxjs";
|
||||
|
||||
@@ -26,7 +24,7 @@ export class ExtensionLockComponentService implements LockComponentService {
|
||||
private readonly biometricStateService = inject(BiometricStateService);
|
||||
|
||||
getPreviousUrl(): string | null {
|
||||
return this.routerService.getPreviousUrl();
|
||||
return this.routerService.getPreviousUrl() ?? null;
|
||||
}
|
||||
|
||||
getBiometricsError(error: any): string | null {
|
||||
@@ -71,7 +69,7 @@ export class ExtensionLockComponentService implements LockComponentService {
|
||||
map(([biometricsStatus, userDecryptionOptions, pinDecryptionAvailable]) => {
|
||||
const unlockOpts: UnlockOptions = {
|
||||
masterPassword: {
|
||||
enabled: userDecryptionOptions.hasMasterPassword,
|
||||
enabled: userDecryptionOptions?.hasMasterPassword,
|
||||
},
|
||||
pin: {
|
||||
enabled: pinDecryptionAvailable,
|
||||
|
||||
@@ -54,7 +54,7 @@ export class DesktopLockComponentService implements LockComponentService {
|
||||
map(([biometricsStatus, userDecryptionOptions, pinDecryptionAvailable]) => {
|
||||
const unlockOpts: UnlockOptions = {
|
||||
masterPassword: {
|
||||
enabled: userDecryptionOptions.hasMasterPassword,
|
||||
enabled: userDecryptionOptions?.hasMasterPassword,
|
||||
},
|
||||
pin: {
|
||||
enabled: pinDecryptionAvailable,
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
// FIXME: Update this file to be type safe and remove this and next line
|
||||
// @ts-strict-ignore
|
||||
import { PinServiceAbstraction } from "@bitwarden/auth/common";
|
||||
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||
import { InternalMasterPasswordServiceAbstraction } from "@bitwarden/common/auth/abstractions/master-password.service.abstraction";
|
||||
@@ -75,26 +73,23 @@ export class ElectronKeyService extends DefaultKeyService {
|
||||
protected override async getKeyFromStorage(
|
||||
keySuffix: KeySuffixOptions,
|
||||
userId?: UserId,
|
||||
): Promise<UserKey> {
|
||||
): Promise<UserKey | null> {
|
||||
return await super.getKeyFromStorage(keySuffix, userId);
|
||||
}
|
||||
|
||||
protected async storeBiometricsProtectedUserKey(
|
||||
userKey: UserKey,
|
||||
userId?: UserId,
|
||||
): Promise<void> {
|
||||
private async storeBiometricsProtectedUserKey(userKey: UserKey, userId: UserId): Promise<void> {
|
||||
// May resolve to null, in which case no client key have is required
|
||||
// TODO: Move to windows implementation
|
||||
const clientEncKeyHalf = await this.getBiometricEncryptionClientKeyHalf(userKey, userId);
|
||||
await this.biometricService.setClientKeyHalfForUser(userId, clientEncKeyHalf);
|
||||
await this.biometricService.setClientKeyHalfForUser(userId, clientEncKeyHalf as string);
|
||||
await this.biometricService.setBiometricProtectedUnlockKeyForUser(userId, userKey.keyB64);
|
||||
}
|
||||
|
||||
protected async shouldStoreKey(keySuffix: KeySuffixOptions, userId?: UserId): Promise<boolean> {
|
||||
protected async shouldStoreKey(keySuffix: KeySuffixOptions, userId: UserId): Promise<boolean> {
|
||||
return await super.shouldStoreKey(keySuffix, userId);
|
||||
}
|
||||
|
||||
protected override async clearAllStoredUserKeys(userId?: UserId): Promise<void> {
|
||||
protected override async clearAllStoredUserKeys(userId: UserId): Promise<void> {
|
||||
await this.biometricService.deleteBiometricUnlockKeyForUser(userId);
|
||||
await super.clearAllStoredUserKeys(userId);
|
||||
}
|
||||
|
||||
@@ -56,6 +56,9 @@ export class RotateableKeySetService {
|
||||
keySet.encryptedPublicKey,
|
||||
oldUserKey,
|
||||
);
|
||||
if (publicKey == null) {
|
||||
throw new Error("failed to rotate key set: could not decrypt public key");
|
||||
}
|
||||
const newEncryptedPublicKey = await this.encryptService.encrypt(publicKey, newUserKey);
|
||||
const newEncryptedUserKey = await this.encryptService.rsaEncrypt(newUserKey.key, publicKey);
|
||||
|
||||
|
||||
@@ -1,16 +1,8 @@
|
||||
// FIXME: Update this file to be type safe and remove this and next line
|
||||
// @ts-strict-ignore
|
||||
import { OrganizationUserResetPasswordWithIdRequest } from "@bitwarden/admin-console/common";
|
||||
import { WebauthnRotateCredentialRequest } from "@bitwarden/common/auth/models/request/webauthn-rotate-credential.request";
|
||||
// FIXME: remove `src` and fix import
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
import { SendWithIdRequest } from "@bitwarden/common/src/tools/send/models/request/send-with-id.request";
|
||||
// FIXME: remove `src` and fix import
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
import { CipherWithIdRequest } from "@bitwarden/common/src/vault/models/request/cipher-with-id.request";
|
||||
// FIXME: remove `src` and fix import
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
import { FolderWithIdRequest } from "@bitwarden/common/src/vault/models/request/folder-with-id.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";
|
||||
|
||||
@@ -24,4 +16,10 @@ export class UpdateKeyRequest {
|
||||
emergencyAccessKeys: EmergencyAccessWithIdRequest[] = [];
|
||||
resetPasswordKeys: OrganizationUserResetPasswordWithIdRequest[] = [];
|
||||
webauthnKeys: WebauthnRotateCredentialRequest[] = [];
|
||||
|
||||
constructor(masterPasswordHash: string, key: string, privateKey: string) {
|
||||
this.masterPasswordHash = masterPasswordHash;
|
||||
this.key = key;
|
||||
this.privateKey = privateKey;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
// FIXME: Update this file to be type safe and remove this and next line
|
||||
// @ts-strict-ignore
|
||||
import { mock, MockProxy } from "jest-mock-extended";
|
||||
import { BehaviorSubject } from "rxjs";
|
||||
|
||||
@@ -10,6 +8,7 @@ import { WebauthnRotateCredentialRequest } from "@bitwarden/common/auth/models/r
|
||||
import { EncryptService } from "@bitwarden/common/key-management/crypto/abstractions/encrypt.service";
|
||||
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
|
||||
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
||||
import { EncString } from "@bitwarden/common/platform/models/domain/enc-string";
|
||||
import { SymmetricCryptoKey } from "@bitwarden/common/platform/models/domain/symmetric-crypto-key";
|
||||
import { SendWithIdRequest } from "@bitwarden/common/tools/send/models/request/send-with-id.request";
|
||||
import { SendService } from "@bitwarden/common/tools/send/services/send.service.abstraction";
|
||||
@@ -24,9 +23,9 @@ import { FolderWithIdRequest } from "@bitwarden/common/vault/models/request/fold
|
||||
import { KeyService } from "@bitwarden/key-management";
|
||||
|
||||
import { OrganizationUserResetPasswordService } from "../../admin-console/organizations/members/services/organization-user-reset-password/organization-user-reset-password.service";
|
||||
import { WebauthnLoginAdminService } from "../core";
|
||||
import { EmergencyAccessService } from "../emergency-access";
|
||||
import { EmergencyAccessWithIdRequest } from "../emergency-access/request/emergency-access-update.request";
|
||||
import { WebauthnLoginAdminService } from "../../auth/core";
|
||||
import { EmergencyAccessService } from "../../auth/emergency-access";
|
||||
import { EmergencyAccessWithIdRequest } from "../../auth/emergency-access/request/emergency-access-update.request";
|
||||
|
||||
import { UserKeyRotationApiService } from "./user-key-rotation-api.service";
|
||||
import { UserKeyRotationService } from "./user-key-rotation.service";
|
||||
@@ -94,7 +93,7 @@ describe("KeyRotationService", () => {
|
||||
});
|
||||
|
||||
describe("rotateUserKeyAndEncryptedData", () => {
|
||||
let privateKey: BehaviorSubject<UserPrivateKey>;
|
||||
let privateKey: BehaviorSubject<UserPrivateKey | null>;
|
||||
|
||||
beforeEach(() => {
|
||||
mockKeyService.makeUserKey.mockResolvedValue([
|
||||
@@ -170,7 +169,10 @@ describe("KeyRotationService", () => {
|
||||
});
|
||||
|
||||
it("throws if user key creation fails", async () => {
|
||||
mockKeyService.makeUserKey.mockResolvedValueOnce([null, null]);
|
||||
mockKeyService.makeUserKey.mockResolvedValueOnce([
|
||||
null as unknown as UserKey,
|
||||
null as unknown as EncString,
|
||||
]);
|
||||
|
||||
await expect(
|
||||
keyRotationService.rotateUserKeyAndEncryptedData("mockMasterPassword", mockUser),
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
// FIXME: Update this file to be type safe and remove this and next line
|
||||
// @ts-strict-ignore
|
||||
import { Injectable } from "@angular/core";
|
||||
import { firstValueFrom } from "rxjs";
|
||||
|
||||
@@ -77,20 +75,16 @@ export class UserKeyRotationService {
|
||||
|
||||
const [newUserKey, newEncUserKey] = await this.keyService.makeUserKey(masterKey);
|
||||
|
||||
if (!newUserKey || !newEncUserKey) {
|
||||
if (newUserKey == null || newEncUserKey == null || newEncUserKey.encryptedString == null) {
|
||||
this.logService.info("[Userkey rotation] User key could not be created. Aborting!");
|
||||
throw new Error("User key could not be created");
|
||||
}
|
||||
|
||||
// Create new request
|
||||
const request = new UpdateKeyRequest();
|
||||
|
||||
// Add new user key
|
||||
request.key = newEncUserKey.encryptedString;
|
||||
// New user key
|
||||
const key = newEncUserKey.encryptedString;
|
||||
|
||||
// Add master key hash
|
||||
const masterPasswordHash = await this.keyService.hashMasterKey(masterPassword, masterKey);
|
||||
request.masterPasswordHash = masterPasswordHash;
|
||||
|
||||
// Get original user key
|
||||
// Note: We distribute the legacy key, but not all domains actually use it. If any of those
|
||||
@@ -101,7 +95,14 @@ export class UserKeyRotationService {
|
||||
this.logService.info("[Userkey rotation] Is legacy user: " + isMasterKey);
|
||||
|
||||
// Add re-encrypted data
|
||||
request.privateKey = await this.encryptPrivateKey(newUserKey, user.id);
|
||||
const privateKey = await this.encryptPrivateKey(newUserKey, user.id);
|
||||
if (privateKey == null) {
|
||||
this.logService.info("[Userkey rotation] Private key could not be encrypted. Aborting!");
|
||||
throw new Error("Private key could not be encrypted");
|
||||
}
|
||||
|
||||
// Create new request
|
||||
const request = new UpdateKeyRequest(masterPasswordHash, key, privateKey);
|
||||
|
||||
const rotatedCiphers = await this.cipherService.getRotatedData(
|
||||
originalUserKey,
|
||||
@@ -172,11 +173,11 @@ export class UserKeyRotationService {
|
||||
private async encryptPrivateKey(
|
||||
newUserKey: UserKey,
|
||||
userId: UserId,
|
||||
): Promise<EncryptedString | null> {
|
||||
): Promise<EncryptedString | undefined> {
|
||||
const privateKey = await firstValueFrom(
|
||||
this.keyService.userPrivateKeyWithLegacySupport$(userId),
|
||||
);
|
||||
if (!privateKey) {
|
||||
if (privateKey == null) {
|
||||
throw new Error("No private key found for user key rotation");
|
||||
}
|
||||
return (await this.encryptService.encrypt(privateKey, newUserKey)).encryptedString;
|
||||
|
||||
@@ -34,8 +34,8 @@ export class WebLockComponentService implements LockComponentService {
|
||||
);
|
||||
}
|
||||
|
||||
getAvailableUnlockOptions$(userId: UserId): Observable<UnlockOptions> {
|
||||
return this.userDecryptionOptionsService.userDecryptionOptionsById$(userId).pipe(
|
||||
getAvailableUnlockOptions$(userId: UserId): Observable<UnlockOptions | null> {
|
||||
return this.userDecryptionOptionsService.userDecryptionOptionsById$(userId)?.pipe(
|
||||
map((userDecryptionOptions: UserDecryptionOptions) => {
|
||||
const unlockOpts: UnlockOptions = {
|
||||
masterPassword: {
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
// FIXME: Update this file to be type safe and remove this and next line
|
||||
// @ts-strict-ignore
|
||||
import { Component } from "@angular/core";
|
||||
import { FormControl, FormGroup, Validators } from "@angular/forms";
|
||||
import { firstValueFrom } from "rxjs";
|
||||
@@ -50,6 +48,9 @@ export class MigrateFromLegacyEncryptionComponent {
|
||||
}
|
||||
|
||||
const activeUser = await firstValueFrom(this.accountService.activeAccount$);
|
||||
if (activeUser == null) {
|
||||
throw new Error("No active user.");
|
||||
}
|
||||
|
||||
const hasUserKey = await this.keyService.hasUserKey(activeUser.id);
|
||||
if (hasUserKey) {
|
||||
@@ -57,7 +58,7 @@ export class MigrateFromLegacyEncryptionComponent {
|
||||
throw new Error("User key already exists, cannot migrate legacy encryption.");
|
||||
}
|
||||
|
||||
const masterPassword = this.formGroup.value.masterPassword;
|
||||
const masterPassword = this.formGroup.value.masterPassword!;
|
||||
|
||||
try {
|
||||
await this.syncService.fullSync(false, true);
|
||||
@@ -73,7 +74,10 @@ export class MigrateFromLegacyEncryptionComponent {
|
||||
this.messagingService.send("logout");
|
||||
} catch (e) {
|
||||
// If the error is due to missing folders, we can delete all folders and try again
|
||||
if (e.message === "All existing folders must be included in the rotation.") {
|
||||
if (
|
||||
e instanceof Error &&
|
||||
e.message === "All existing folders must be included in the rotation."
|
||||
) {
|
||||
const deleteFolders = await this.dialogService.openSimpleDialog({
|
||||
type: "warning",
|
||||
title: { key: "encryptionKeyUpdateCannotProceed" },
|
||||
|
||||
Reference in New Issue
Block a user