1
0
mirror of https://github.com/bitwarden/browser synced 2026-02-10 13:40:06 +00:00

Key Management in crypto with TS strict

This commit is contained in:
Maciej Zieniuk
2025-02-27 17:33:38 +00:00
parent a5ea8066a3
commit face84937e
15 changed files with 60 additions and 44 deletions

View File

@@ -350,10 +350,10 @@ export class NativeMessagingBackground {
await this.secureCommunication();
}
return await this.encryptService.encrypt(
return (await this.encryptService.encrypt(
JSON.stringify(message),
this.secureChannel!.sharedSecret!,
);
))!;
}
private postMessage(message: OuterMessage, messageId?: number) {
@@ -394,11 +394,11 @@ export class NativeMessagingBackground {
return;
}
message = JSON.parse(
await this.encryptService.decryptToUtf8(
(await this.encryptService.decryptToUtf8(
rawMessage as EncString,
this.secureChannel.sharedSecret,
"ipc-desktop-ipc-channel-key",
),
))!,
);
} else {
message = rawMessage as ReceiveMessage;

View File

@@ -27,7 +27,7 @@ export class RotateableKeySetService {
const rawPublicKey = Utils.fromB64ToArray(publicKey);
const encryptedUserKey = await this.encryptService.rsaEncrypt(userKey.key, rawPublicKey);
const encryptedPublicKey = await this.encryptService.encrypt(rawPublicKey, userKey);
return new RotateableKeySet(encryptedUserKey, encryptedPublicKey, encryptedPrivateKey);
return new RotateableKeySet(encryptedUserKey, encryptedPublicKey!, encryptedPrivateKey);
}
/**
@@ -64,7 +64,7 @@ export class RotateableKeySetService {
const newRotateableKeySet = new RotateableKeySet<ExternalKey>(
newEncryptedUserKey,
newEncryptedPublicKey,
newEncryptedPublicKey!,
keySet.encryptedPrivateKey,
);

View File

@@ -180,6 +180,6 @@ export class UserKeyRotationService {
if (privateKey == null) {
throw new Error("No private key found for user key rotation");
}
return (await this.encryptService.encrypt(privateKey, newUserKey)).encryptedString;
return (await this.encryptService.encrypt(privateKey, newUserKey))!.encryptedString;
}
}

View File

@@ -6,21 +6,34 @@ import { EncString } from "@bitwarden/common/platform/models/domain/enc-string";
import { SymmetricCryptoKey } from "@bitwarden/common/platform/models/domain/symmetric-crypto-key";
export abstract class EncryptService {
abstract encrypt(plainValue: string | Uint8Array, key: SymmetricCryptoKey): Promise<EncString>;
/**
* Encrypts a string to an EncString.
* @throws Error when {@link key} is null
* @param plainValue - The string to encrypt
* @param key - The key to encrypt the string with
* @returns The encrypted EncString. Returns null when key has mac key but payload is missing mac bytes or when key encryption type does not match payload encryption type or when MAC comparison failed.
*/
abstract encrypt(
plainValue: string | Uint8Array,
key: SymmetricCryptoKey,
): Promise<EncString | null>;
abstract encryptToBytes(plainValue: Uint8Array, key: SymmetricCryptoKey): Promise<EncArrayBuffer>;
/**
* Decrypts an EncString to a string
* @param encString - The EncString to decrypt
* @param key - The key to decrypt the EncString with
* @param decryptTrace - A string to identify the context of the object being decrypted. This can include: field name, encryption type, cipher id, key type, but should not include
* sensitive information like encryption keys or data. This is used for logging when decryption errors occur in order to identify what failed to decrypt
* @returns The decrypted string
* @returns The decrypted string. Returns null when {@link key} is null or when {@link encString}'s ${@link EncString.data} or ${@link EncString.iv} is null or when key has mac key but payload is missing mac bytes or when key encryption type does not match payload encryption type or when MAC comparison failed.
*/
abstract decryptToUtf8(
encString: EncString,
key: SymmetricCryptoKey,
decryptTrace?: string,
): Promise<string>;
): Promise<string | null>;
/**
* Decrypts an Encrypted object to a Uint8Array
* @param encThing - The Encrypted object to decrypt
@@ -34,9 +47,13 @@ export abstract class EncryptService {
key: SymmetricCryptoKey,
decryptTrace?: string,
): Promise<Uint8Array | null>;
abstract rsaEncrypt(data: Uint8Array, publicKey: Uint8Array): Promise<EncString>;
abstract rsaDecrypt(data: EncString, privateKey: Uint8Array): Promise<Uint8Array>;
abstract resolveLegacyKey(key: SymmetricCryptoKey, encThing: Encrypted): SymmetricCryptoKey;
/**
* @deprecated Replaced by BulkEncryptService, remove once the feature is tested and the featureflag PM-4154-multi-worker-encryption-service is removed
* @param items The items to decrypt
@@ -46,6 +63,7 @@ export abstract class EncryptService {
items: Decryptable<T>[],
key: SymmetricCryptoKey,
): Promise<T[]>;
/**
* Generates a base64-encoded hash of the given value
* @param value The value to hash

View File

@@ -1,5 +1,3 @@
// FIXME: Update this file to be type safe and remove this and next line
// @ts-strict-ignore
import { firstValueFrom, fromEvent, filter, map, takeUntil, defaultIfEmpty, Subject } from "rxjs";
import { Jsonify } from "type-fest";
@@ -117,7 +115,7 @@ export class BulkEncryptServiceImplementation implements BulkEncryptService {
worker.postMessage(JSON.stringify(request));
results.push(
firstValueFrom(
fromEvent(worker, "message").pipe(
fromEvent<MessageEvent>(worker, "message").pipe(
filter((response: MessageEvent) => response.data?.id === request.id),
map((response) => JSON.parse(response.data.items)),
map((items) =>

View File

@@ -1,5 +1,3 @@
// FIXME: Update this file to be type safe and remove this and next line
// @ts-strict-ignore
import { CryptoFunctionService } from "@bitwarden/common/platform/abstractions/crypto-function.service";
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
import {
@@ -24,13 +22,16 @@ export class EncryptServiceImplementation implements EncryptService {
protected logMacFailures: boolean,
) {}
async encrypt(plainValue: string | Uint8Array, key: SymmetricCryptoKey): Promise<EncString> {
async encrypt(
plainValue: string | Uint8Array,
key: SymmetricCryptoKey,
): Promise<EncString | null> {
if (key == null) {
throw new Error("No encryption key provided.");
}
if (plainValue == null) {
return Promise.resolve(null);
return null;
}
let plainBuf: Uint8Array;
@@ -43,7 +44,7 @@ export class EncryptServiceImplementation implements EncryptService {
const encObj = await this.aesEncrypt(plainBuf, key);
const iv = Utils.fromBufferToB64(encObj.iv);
const data = Utils.fromBufferToB64(encObj.data);
const mac = encObj.mac != null ? Utils.fromBufferToB64(encObj.mac) : null;
const mac = encObj.mac != null ? Utils.fromBufferToB64(encObj.mac) : undefined;
return new EncString(encObj.key.encType, data, iv, mac);
}
@@ -73,10 +74,16 @@ export class EncryptServiceImplementation implements EncryptService {
encString: EncString,
key: SymmetricCryptoKey,
decryptContext: string = "no context",
): Promise<string> {
): Promise<string | null> {
if (key == null) {
throw new Error("No key provided for decryption.");
}
if (encString?.data == null) {
throw new Error("No data provided for decryption.");
}
if (encString?.iv == null) {
throw new Error("No initialization vector provided for decryption.");
}
key = this.resolveLegacyKey(key, encString);
@@ -106,7 +113,7 @@ export class EncryptServiceImplementation implements EncryptService {
const fastParams = this.cryptoFunctionService.aesDecryptFastParameters(
encString.data,
encString.iv,
encString.mac,
encString.mac ?? null,
key,
);
if (fastParams.macKey != null && fastParams.mac != null) {
@@ -301,6 +308,7 @@ export class EncryptServiceImplementation implements EncryptService {
/**
* Transform into new key for the old encrypt-then-mac scheme if required, otherwise return the current key unchanged
* @param key The key to transform
* @param encThing The encrypted object (e.g. encString or encArrayBuffer) that you want to decrypt
*/
resolveLegacyKey(key: SymmetricCryptoKey, encThing: Encrypted): SymmetricCryptoKey {

View File

@@ -1,5 +1,3 @@
// FIXME: Update this file to be type safe and remove this and next line
// @ts-strict-ignore
import { Jsonify } from "type-fest";
import { Decryptable } from "@bitwarden/common/platform/interfaces/decryptable.interface";

View File

@@ -1,5 +1,3 @@
// FIXME: Update this file to be type safe and remove this and next line
// @ts-strict-ignore
import { BulkEncryptService } from "@bitwarden/common/key-management/crypto/abstractions/bulk-encrypt.service";
import { Decryptable } from "@bitwarden/common/platform/interfaces/decryptable.interface";
import { InitializerMetadata } from "@bitwarden/common/platform/interfaces/initializer-metadata.interface";
@@ -11,7 +9,7 @@ import { EncryptService } from "../abstractions/encrypt.service";
* @deprecated For the feature flag from PM-4154, remove once feature is rolled out
*/
export class FallbackBulkEncryptService implements BulkEncryptService {
private featureFlagEncryptService: BulkEncryptService;
private featureFlagEncryptService?: BulkEncryptService;
constructor(protected encryptService: EncryptService) {}

View File

@@ -1,5 +1,3 @@
// FIXME: Update this file to be type safe and remove this and next line
// @ts-strict-ignore
import { defaultIfEmpty, filter, firstValueFrom, fromEvent, map, Subject, takeUntil } from "rxjs";
import { Jsonify } from "type-fest";
@@ -18,7 +16,7 @@ const workerTTL = 3 * 60000; // 3 minutes
* @deprecated Replaced by BulkEncryptionService (PM-4154)
*/
export class MultithreadEncryptServiceImplementation extends EncryptServiceImplementation {
private worker: Worker;
private worker?: Worker;
private timeout: any;
private clear$ = new Subject<void>();
@@ -56,7 +54,7 @@ export class MultithreadEncryptServiceImplementation extends EncryptServiceImple
this.worker.postMessage(JSON.stringify(request));
return await firstValueFrom(
fromEvent(this.worker, "message").pipe(
fromEvent<MessageEvent>(this.worker, "message").pipe(
filter((response: MessageEvent) => response.data?.id === request.id),
map((response) => JSON.parse(response.data.items)),
map((items) =>
@@ -74,7 +72,7 @@ export class MultithreadEncryptServiceImplementation extends EncryptServiceImple
private clear() {
this.clear$.next();
this.worker?.terminate();
this.worker = null;
this.worker = undefined;
this.clearTimeout();
}

View File

@@ -1,5 +1,3 @@
// FIXME: Update this file to be type safe and remove this and next line
// @ts-strict-ignore
import { firstValueFrom, map, timeout } from "rxjs";
import { PinServiceAbstraction } from "@bitwarden/auth/common";
@@ -21,7 +19,7 @@ export class DefaultProcessReloadService implements ProcessReloadServiceAbstract
constructor(
private pinService: PinServiceAbstraction,
private messagingService: MessagingService,
private reloadCallback: () => Promise<void> = null,
private reloadCallback: (() => Promise<void>) | null = null,
private vaultTimeoutSettingsService: VaultTimeoutSettingsService,
private biometricStateService: BiometricStateService,
private accountService: AccountService,

View File

@@ -49,7 +49,7 @@ export abstract class CryptoFunctionService {
abstract aesDecryptFastParameters(
data: string,
iv: string,
mac: string,
mac: string | null,
key: SymmetricCryptoKey,
): CbcDecryptParameters<Uint8Array | string>;
abstract aesDecryptFast({

View File

@@ -8,8 +8,8 @@ export enum EncryptionType {
Rsa2048_OaepSha1_HmacSha256_B64 = 6,
}
export function encryptionTypeToString(encryptionType: EncryptionType): string {
if (encryptionType in EncryptionType) {
export function encryptionTypeToString(encryptionType?: EncryptionType): string {
if (encryptionType != null && encryptionType in EncryptionType) {
return EncryptionType[encryptionType];
} else {
return "Unknown encryption type " + encryptionType;

View File

@@ -4,8 +4,8 @@ import { EncryptService } from "../../key-management/crypto/abstractions/encrypt
export class ContainerService {
constructor(
private keyService: KeyService,
private encryptService: EncryptService,
private keyService: KeyService | null,
private encryptService: EncryptService | null,
) {}
attachToGlobal(global: any) {

View File

@@ -545,7 +545,7 @@ export class DefaultKeyService implements KeyServiceAbstraction {
const keyPair = await this.cryptoFunctionService.rsaGenerateKeyPair(2048);
const publicB64 = Utils.fromBufferToB64(keyPair[0]);
const privateEnc = await this.encryptService.encrypt(keyPair[1], key);
return [publicB64, privateEnc];
return [publicB64, privateEnc!];
}
/**
@@ -732,10 +732,10 @@ export class DefaultKeyService implements KeyServiceAbstraction {
const storePin = await this.shouldStoreKey(KeySuffixOptions.Pin, userId);
if (storePin) {
// Decrypt userKeyEncryptedPin with user key
const pin = await this.encryptService.decryptToUtf8(
const pin = (await this.encryptService.decryptToUtf8(
(await this.pinService.getUserKeyEncryptedPin(userId))!,
key,
);
))!;
const pinKeyEncryptedUserKey = await this.pinService.createPinKeyEncryptedUserKey(
pin,
@@ -827,9 +827,9 @@ export class DefaultKeyService implements KeyServiceAbstraction {
let protectedSymKey: EncString;
if (encryptionKey.key.byteLength === 32) {
const stretchedEncryptionKey = await this.keyGenerationService.stretchKey(encryptionKey);
protectedSymKey = await this.encryptService.encrypt(newSymKey, stretchedEncryptionKey);
protectedSymKey = (await this.encryptService.encrypt(newSymKey, stretchedEncryptionKey))!;
} else if (encryptionKey.key.byteLength === 64) {
protectedSymKey = await this.encryptService.encrypt(newSymKey, encryptionKey);
protectedSymKey = (await this.encryptService.encrypt(newSymKey, encryptionKey))!;
} else {
throw new Error("Invalid key size.");
}

View File

@@ -19,7 +19,7 @@ export class LegacyPasswordHistoryDecryptor {
const promises = (history ?? []).map(async (item) => {
const encrypted = new EncString(item.password);
const decrypted = await this.encryptService.decryptToUtf8(encrypted, key);
const decrypted = (await this.encryptService.decryptToUtf8(encrypted, key))!;
return new GeneratedPasswordHistory(decrypted, item.date);
});