1
0
mirror of https://github.com/bitwarden/browser synced 2026-02-20 03:13:55 +00:00

Merge remote-tracking branch 'origin/sdk-encrypt-service' into km/cose

This commit is contained in:
Bernd Schoolmann
2025-03-06 12:04:27 +01:00
130 changed files with 2044 additions and 986 deletions

View File

@@ -1,3 +1,5 @@
import { ServerConfig } from "../platform/abstractions/config/server-config";
/**
* Feature flags.
*
@@ -5,7 +7,6 @@
*/
export enum FeatureFlag {
/* Admin Console Team */
ProviderClientVaultPrivacyBanner = "ac-2833-provider-client-vault-privacy-banner",
AccountDeprovisioning = "pm-10308-account-deprovisioning",
VerifiedSsoDomainEndpoint = "pm-12337-refactor-sso-details-endpoint",
LimitItemDeletion = "pm-15493-restrict-item-deletion-to-can-manage-permission",
@@ -23,6 +24,9 @@ export enum FeatureFlag {
NotificationRefresh = "notification-refresh",
UseTreeWalkerApiForPageDetailsCollection = "use-tree-walker-api-for-page-details-collection",
/* Key Management */
UseSDKForDecryption = "use-sdk-for-decryption",
/* Tools */
ItemShare = "item-share",
CriticalApps = "pm-14466-risk-insights-critical-application",
@@ -64,7 +68,6 @@ const FALSE = false as boolean;
*/
export const DefaultFeatureFlagValue = {
/* Admin Console Team */
[FeatureFlag.ProviderClientVaultPrivacyBanner]: FALSE,
[FeatureFlag.AccountDeprovisioning]: FALSE,
[FeatureFlag.VerifiedSsoDomainEndpoint]: FALSE,
[FeatureFlag.LimitItemDeletion]: FALSE,
@@ -82,6 +85,9 @@ export const DefaultFeatureFlagValue = {
[FeatureFlag.NotificationRefresh]: FALSE,
[FeatureFlag.UseTreeWalkerApiForPageDetailsCollection]: FALSE,
/* Key Management */
[FeatureFlag.UseSDKForDecryption]: FALSE,
/* Tools */
[FeatureFlag.ItemShare]: FALSE,
[FeatureFlag.CriticalApps]: FALSE,
@@ -113,3 +119,14 @@ export const DefaultFeatureFlagValue = {
export type DefaultFeatureFlagValueType = typeof DefaultFeatureFlagValue;
export type FeatureFlagValueType<Flag extends FeatureFlag> = DefaultFeatureFlagValueType[Flag];
export function getFeatureFlagValue<Flag extends FeatureFlag>(
serverConfig: ServerConfig | null,
flag: Flag,
) {
if (serverConfig?.featureStates == null || serverConfig.featureStates[flag] == null) {
return DefaultFeatureFlagValue[flag];
}
return serverConfig.featureStates[flag] as FeatureFlagValueType<Flag>;
}

View File

@@ -1,10 +1,13 @@
import { Decryptable } from "@bitwarden/common/platform/interfaces/decryptable.interface";
import { InitializerMetadata } from "@bitwarden/common/platform/interfaces/initializer-metadata.interface";
import { SymmetricCryptoKey } from "@bitwarden/common/platform/models/domain/symmetric-crypto-key";
import { OnServerConfigChange } from "../../../platform/abstractions/config/config.service";
import { ServerConfig } from "../../../platform/abstractions/config/server-config";
import { Decryptable } from "../../../platform/interfaces/decryptable.interface";
import { InitializerMetadata } from "../../../platform/interfaces/initializer-metadata.interface";
import { SymmetricCryptoKey } from "../../../platform/models/domain/symmetric-crypto-key";
export abstract class BulkEncryptService {
export abstract class BulkEncryptService implements OnServerConfigChange {
abstract decryptItems<T extends InitializerMetadata>(
items: Decryptable<T>[],
key: SymmetricCryptoKey,
): Promise<T[]>;
abstract onServerConfigChange(newConfig: ServerConfig): void;
}

View File

@@ -1,3 +1,5 @@
import { OnServerConfigChange } from "@bitwarden/common/platform/abstractions/config/config.service";
import { ServerConfig } from "@bitwarden/common/platform/abstractions/config/server-config";
import { Decryptable } from "@bitwarden/common/platform/interfaces/decryptable.interface";
import { Encrypted } from "@bitwarden/common/platform/interfaces/encrypted";
import { InitializerMetadata } from "@bitwarden/common/platform/interfaces/initializer-metadata.interface";
@@ -5,7 +7,7 @@ import { EncArrayBuffer } from "@bitwarden/common/platform/models/domain/enc-arr
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 {
export abstract class EncryptService implements OnServerConfigChange {
abstract encrypt(plainValue: string | Uint8Array, key: SymmetricCryptoKey): Promise<EncString>;
abstract encryptToBytes(plainValue: Uint8Array, key: SymmetricCryptoKey): Promise<EncArrayBuffer>;
/**
@@ -55,4 +57,5 @@ export abstract class EncryptService {
value: string | Uint8Array,
algorithm: "sha1" | "sha256" | "sha512",
): Promise<string>;
abstract onServerConfigChange(newConfig: ServerConfig): void;
}

View File

@@ -12,6 +12,10 @@ import { Utils } from "@bitwarden/common/platform/misc/utils";
import { SymmetricCryptoKey } from "@bitwarden/common/platform/models/domain/symmetric-crypto-key";
import { getClassInitializer } from "@bitwarden/common/platform/services/cryptography/get-class-initializer";
import { ServerConfig } from "../../../platform/abstractions/config/server-config";
import { buildDecryptMessage, buildSetConfigMessage } from "./encrypt.worker";
// TTL (time to live) is not strictly required but avoids tying up memory resources if inactive
const workerTTL = 60000; // 1 minute
const maxWorkers = 8;
@@ -57,6 +61,13 @@ export class BulkEncryptServiceImplementation implements BulkEncryptService {
return decryptedItems;
}
onServerConfigChange(newConfig: ServerConfig): void {
this.workers.forEach((worker) => {
const request = buildSetConfigMessage({ newConfig });
worker.postMessage(request);
});
}
/**
* Sends items to a set of web workers to decrypt them. This utilizes multiple workers to decrypt items
* faster without interrupting other operations (e.g. updating UI).
@@ -108,17 +119,18 @@ export class BulkEncryptServiceImplementation implements BulkEncryptService {
itemsForWorker.push(...items.slice(end));
}
const request = {
id: Utils.newGuid(),
const id = Utils.newGuid();
const request = buildDecryptMessage({
id,
items: itemsForWorker,
key: key,
};
});
worker.postMessage(JSON.stringify(request));
worker.postMessage(request);
results.push(
firstValueFrom(
fromEvent(worker, "message").pipe(
filter((response: MessageEvent) => response.data?.id === request.id),
filter((response: MessageEvent) => response.data?.id === id),
map((response) => JSON.parse(response.data.items)),
map((items) =>
items.map((jsonItem: Jsonify<T>) => {

View File

@@ -14,16 +14,31 @@ import { EncArrayBuffer } from "@bitwarden/common/platform/models/domain/enc-arr
import { EncString } from "@bitwarden/common/platform/models/domain/enc-string";
import { EncryptedObject } from "@bitwarden/common/platform/models/domain/encrypted-object";
import { SymmetricCryptoKey } from "@bitwarden/common/platform/models/domain/symmetric-crypto-key";
import { PureCrypto } from "@bitwarden/sdk-internal";
import {
DefaultFeatureFlagValue,
FeatureFlag,
getFeatureFlagValue,
} from "../../../enums/feature-flag.enum";
import { ServerConfig } from "../../../platform/abstractions/config/server-config";
import { EncryptService } from "../abstractions/encrypt.service";
export class EncryptServiceImplementation implements EncryptService {
private useSDKForDecryption: boolean = DefaultFeatureFlagValue[FeatureFlag.UseSDKForDecryption];
constructor(
protected cryptoFunctionService: CryptoFunctionService,
protected logService: LogService,
protected logMacFailures: boolean,
) {}
onServerConfigChange(newConfig: ServerConfig): void {
const old = this.useSDKForDecryption;
this.useSDKForDecryption = getFeatureFlagValue(newConfig, FeatureFlag.UseSDKForDecryption);
this.logService.debug("updated sdk decryption flag", old, this.useSDKForDecryption);
}
async encrypt(plainValue: string | Uint8Array, key: SymmetricCryptoKey): Promise<EncString> {
if (key == null) {
throw new Error("No encryption key provided.");
@@ -53,20 +68,7 @@ export class EncryptServiceImplementation implements EncryptService {
}
const encValue = await this.aesEncrypt(plainValue, key);
let macLen = 0;
if (encValue.mac != null) {
macLen = encValue.mac.byteLength;
}
const encBytes = new Uint8Array(1 + encValue.iv.byteLength + macLen + encValue.data.byteLength);
encBytes.set([encValue.key.encType]);
encBytes.set(new Uint8Array(encValue.iv), 1);
if (encValue.mac != null) {
encBytes.set(new Uint8Array(encValue.mac), 1 + encValue.iv.byteLength);
}
encBytes.set(new Uint8Array(encValue.data), 1 + encValue.iv.byteLength + macLen);
return new EncArrayBuffer(encBytes);
return EncArrayBuffer.fromParts(encValue.key.encType, encValue.iv, encValue.data, encValue.mac);
}
async decryptToUtf8(
@@ -74,6 +76,15 @@ export class EncryptServiceImplementation implements EncryptService {
key: SymmetricCryptoKey,
decryptContext: string = "no context",
): Promise<string> {
if (this.useSDKForDecryption) {
this.logService.debug("decrypting with SDK");
if (encString == null || encString.encryptedString == null) {
throw new Error("encString is null or undefined");
}
return PureCrypto.symmetric_decrypt(encString.encryptedString, key.keyB64);
}
this.logService.debug("decrypting with javascript");
if (key == null) {
throw new Error("No key provided for decryption.");
}
@@ -137,6 +148,25 @@ export class EncryptServiceImplementation implements EncryptService {
key: SymmetricCryptoKey,
decryptContext: string = "no context",
): Promise<Uint8Array | null> {
if (this.useSDKForDecryption) {
this.logService.debug("decrypting bytes with SDK");
if (
encThing.encryptionType == null ||
encThing.ivBytes == null ||
encThing.dataBytes == null
) {
throw new Error("Cannot decrypt, missing type, IV, or data bytes.");
}
const buffer = EncArrayBuffer.fromParts(
encThing.encryptionType,
encThing.ivBytes,
encThing.dataBytes,
encThing.macBytes,
).buffer;
return PureCrypto.symmetric_decrypt_array_buffer(buffer, key.keyB64);
}
this.logService.debug("decrypting bytes with javascript");
if (key == null) {
throw new Error("No encryption key provided.");
}

View File

@@ -9,19 +9,48 @@ import { ContainerService } from "@bitwarden/common/platform/services/container.
import { getClassInitializer } from "@bitwarden/common/platform/services/cryptography/get-class-initializer";
import { WebCryptoFunctionService } from "@bitwarden/common/platform/services/web-crypto-function.service";
import { ServerConfig } from "../../../platform/abstractions/config/server-config";
import { LogService } from "../../../platform/abstractions/log.service";
import { EncryptServiceImplementation } from "./encrypt.service.implementation";
const workerApi: Worker = self as any;
let inited = false;
let encryptService: EncryptServiceImplementation;
let logService: LogService;
const DECRYPT_COMMAND_SHELL = Object.freeze({ command: "decrypt" });
const SET_CONFIG_COMMAND_SHELL = Object.freeze({ command: "setConfig" });
type DecryptCommandData = {
id: string;
items: Jsonify<Decryptable<any>>[];
key: Jsonify<SymmetricCryptoKey>;
};
type SetConfigCommandData = { newConfig: ServerConfig };
export function buildDecryptMessage(data: DecryptCommandData): string {
return JSON.stringify({
...data,
...DECRYPT_COMMAND_SHELL,
});
}
export function buildSetConfigMessage(data: SetConfigCommandData): string {
return JSON.stringify({
...data,
...SET_CONFIG_COMMAND_SHELL,
});
}
/**
* Bootstrap the worker environment with services required for decryption
*/
export function init() {
const cryptoFunctionService = new WebCryptoFunctionService(self);
const logService = new ConsoleLogService(false);
logService = new ConsoleLogService(false);
encryptService = new EncryptServiceImplementation(cryptoFunctionService, logService, true);
const bitwardenContainerService = new ContainerService(null, encryptService);
@@ -39,11 +68,20 @@ workerApi.addEventListener("message", async (event: { data: string }) => {
}
const request: {
id: string;
items: Jsonify<Decryptable<any>>[];
key: Jsonify<SymmetricCryptoKey>;
command: string;
} = JSON.parse(event.data);
switch (request.command) {
case DECRYPT_COMMAND_SHELL.command:
return await handleDecrypt(request as unknown as DecryptCommandData);
case SET_CONFIG_COMMAND_SHELL.command:
return await handleSetConfig(request as unknown as SetConfigCommandData);
default:
logService.error(`unknown worker command`, request.command, request);
}
});
async function handleDecrypt(request: DecryptCommandData) {
const key = SymmetricCryptoKey.fromJSON(request.key);
const items = request.items.map((jsonItem) => {
const initializer = getClassInitializer<Decryptable<any>>(jsonItem.initializerKey);
@@ -55,4 +93,8 @@ workerApi.addEventListener("message", async (event: { data: string }) => {
id: request.id,
items: JSON.stringify(result),
});
});
}
async function handleSetConfig(request: SetConfigCommandData) {
encryptService.onServerConfigChange(request.newConfig);
}

View File

@@ -1,10 +1,10 @@
// 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";
import { SymmetricCryptoKey } from "@bitwarden/common/platform/models/domain/symmetric-crypto-key";
import { BulkEncryptService } from "../../../key-management/crypto/abstractions/bulk-encrypt.service";
import { ServerConfig } from "../../../platform/abstractions/config/server-config";
import { Decryptable } from "../../../platform/interfaces/decryptable.interface";
import { InitializerMetadata } from "../../../platform/interfaces/initializer-metadata.interface";
import { SymmetricCryptoKey } from "../../../platform/models/domain/symmetric-crypto-key";
import { EncryptService } from "../abstractions/encrypt.service";
/**
@@ -33,4 +33,8 @@ export class FallbackBulkEncryptService implements BulkEncryptService {
async setFeatureFlagEncryptService(featureFlagEncryptService: BulkEncryptService) {
this.featureFlagEncryptService = featureFlagEncryptService;
}
onServerConfigChange(newConfig: ServerConfig): void {
(this.featureFlagEncryptService ?? this.encryptService).onServerConfigChange(newConfig);
}
}

View File

@@ -9,7 +9,10 @@ import { Utils } from "@bitwarden/common/platform/misc/utils";
import { SymmetricCryptoKey } from "@bitwarden/common/platform/models/domain/symmetric-crypto-key";
import { getClassInitializer } from "@bitwarden/common/platform/services/cryptography/get-class-initializer";
import { ServerConfig } from "../../../platform/abstractions/config/server-config";
import { EncryptServiceImplementation } from "./encrypt.service.implementation";
import { buildDecryptMessage, buildSetConfigMessage } from "./encrypt.worker";
// TTL (time to live) is not strictly required but avoids tying up memory resources if inactive
const workerTTL = 3 * 60000; // 3 minutes
@@ -47,17 +50,18 @@ export class MultithreadEncryptServiceImplementation extends EncryptServiceImple
this.restartTimeout();
const request = {
id: Utils.newGuid(),
const id = Utils.newGuid();
const request = buildDecryptMessage({
id,
items: items,
key: key,
};
});
this.worker.postMessage(JSON.stringify(request));
this.worker.postMessage(request);
return await firstValueFrom(
fromEvent(this.worker, "message").pipe(
filter((response: MessageEvent) => response.data?.id === request.id),
filter((response: MessageEvent) => response.data?.id === id),
map((response) => JSON.parse(response.data.items)),
map((items) =>
items.map((jsonItem: Jsonify<T>) => {
@@ -71,6 +75,15 @@ export class MultithreadEncryptServiceImplementation extends EncryptServiceImple
);
}
override onServerConfigChange(newConfig: ServerConfig): void {
super.onServerConfigChange(newConfig);
if (this.worker !== null) {
const request = buildSetConfigMessage({ newConfig });
this.worker.postMessage(request);
}
}
private clear() {
this.clear$.next();
this.worker?.terminate();

View File

@@ -1,6 +1,6 @@
// FIXME: Update this file to be type safe and remove this and next line
// @ts-strict-ignore
import { Observable } from "rxjs";
import { Observable, Subscription } from "rxjs";
import { SemVer } from "semver";
import { FeatureFlag, FeatureFlagValueType } from "../../../enums/feature-flag.enum";
@@ -10,6 +10,8 @@ import { Region } from "../environment.service";
import { ServerConfig } from "./server-config";
export type ConfigCallback = (serverConfig: ServerConfig) => void;
export abstract class ConfigService {
/** The server config of the currently active user */
serverConfig$: Observable<ServerConfig | null>;
@@ -54,4 +56,10 @@ export abstract class ConfigService {
* Triggers a check that the config for the currently active user is up-to-date. If it is not, it will be fetched from the server and stored.
*/
abstract ensureConfigFetched(): Promise<void>;
abstract broadcastConfigChangesTo(...listeners: OnServerConfigChange[]): Subscription;
}
export interface OnServerConfigChange {
onServerConfigChange(newConfig: ServerConfig): void;
}

View File

@@ -0,0 +1,15 @@
import {
AsymmetricEncryptionTypes,
EncryptionType,
SymmetricEncryptionTypes,
} from "./encryption-type.enum";
describe("EncryptionType", () => {
it("classifies all types as symmetric or asymmetric", () => {
const nSymmetric = SymmetricEncryptionTypes.length;
const nAsymmetric = AsymmetricEncryptionTypes.length;
const nTotal = nSymmetric + nAsymmetric;
// enums are indexable by string and number
expect(Object.keys(EncryptionType).length).toEqual(nTotal * 2);
});
});

View File

@@ -8,6 +8,19 @@ export enum EncryptionType {
Rsa2048_OaepSha1_HmacSha256_B64 = 6,
}
export const SymmetricEncryptionTypes = [
EncryptionType.AesCbc256_B64,
EncryptionType.AesCbc128_HmacSha256_B64,
EncryptionType.AesCbc256_HmacSha256_B64,
] as const;
export const AsymmetricEncryptionTypes = [
EncryptionType.Rsa2048_OaepSha256_B64,
EncryptionType.Rsa2048_OaepSha1_B64,
EncryptionType.Rsa2048_OaepSha256_HmacSha256_B64,
EncryptionType.Rsa2048_OaepSha1_HmacSha256_B64,
] as const;
export function encryptionTypeToString(encryptionType: EncryptionType): string {
if (encryptionType in EncryptionType) {
return EncryptionType[encryptionType];

View File

@@ -2,7 +2,7 @@ import { EncryptionType } from "../enums";
export interface Encrypted {
encryptionType?: EncryptionType;
dataBytes: Uint8Array;
macBytes: Uint8Array;
ivBytes: Uint8Array;
dataBytes: Uint8Array | null;
macBytes: Uint8Array | null | undefined;
ivBytes: Uint8Array | null;
}

View File

@@ -1,5 +1,10 @@
import { makeStaticByteArray } from "../../../../spec";
import { EncryptionType } from "../../enums";
import {
EncryptionType,
SymmetricEncryptionTypes,
AsymmetricEncryptionTypes,
encryptionTypeToString,
} from "../../enums";
import { EncArrayBuffer } from "./enc-array-buffer";
@@ -71,4 +76,66 @@ describe("encArrayBuffer", () => {
const bytes = makeStaticByteArray(50, 9);
expect(() => new EncArrayBuffer(bytes)).toThrow("Error parsing encrypted ArrayBuffer");
});
describe("fromParts factory", () => {
const plainValue = makeStaticByteArray(16, 1);
it("throws if required data is null", () => {
expect(() =>
EncArrayBuffer.fromParts(EncryptionType.AesCbc128_HmacSha256_B64, plainValue, null!, null),
).toThrow("encryptionType, iv, and data must be provided");
expect(() =>
EncArrayBuffer.fromParts(EncryptionType.AesCbc128_HmacSha256_B64, null!, plainValue, null),
).toThrow("encryptionType, iv, and data must be provided");
expect(() => EncArrayBuffer.fromParts(null!, plainValue, plainValue, null)).toThrow(
"encryptionType, iv, and data must be provided",
);
});
it.each(SymmetricEncryptionTypes.map((t) => encryptionTypeToString(t)))(
"works for %s",
async (typeName) => {
const type = EncryptionType[typeName as keyof typeof EncryptionType];
const iv = plainValue;
const mac = type === EncryptionType.AesCbc256_B64 ? null : makeStaticByteArray(32, 20);
const data = plainValue;
const actual = EncArrayBuffer.fromParts(type, iv, data, mac);
expect(actual.encryptionType).toEqual(type);
expect(actual.ivBytes).toEqual(iv);
expect(actual.macBytes).toEqual(mac);
expect(actual.dataBytes).toEqual(data);
},
);
it.each(SymmetricEncryptionTypes.filter((t) => t !== EncryptionType.AesCbc256_B64))(
"validates mac length for %s",
(type) => {
const iv = plainValue;
const mac = makeStaticByteArray(1, 20);
const data = plainValue;
expect(() => EncArrayBuffer.fromParts(type, iv, data, mac)).toThrow("Invalid MAC length");
},
);
it.each(SymmetricEncryptionTypes.map((t) => encryptionTypeToString(t)))(
"requires or forbids mac for %s",
async (typeName) => {
const type = EncryptionType[typeName as keyof typeof EncryptionType];
const iv = makeStaticByteArray(16, 10);
const mac = type === EncryptionType.AesCbc256_B64 ? makeStaticByteArray(32, 20) : null;
const data = plainValue;
expect(() => EncArrayBuffer.fromParts(type, iv, data, mac)).toThrow();
},
);
it.each(AsymmetricEncryptionTypes)("throws for async type %s", (type) => {
expect(() => EncArrayBuffer.fromParts(type, plainValue, plainValue, null)).toThrow(
`Unknown EncryptionType ${type} for EncArrayBuffer.fromParts`,
);
});
});
});

View File

@@ -1,5 +1,3 @@
// FIXME: Update this file to be type safe and remove this and next line
// @ts-strict-ignore
import { Utils } from "../../../platform/misc/utils";
import { EncryptionType } from "../../enums";
import { Encrypted } from "../../interfaces/encrypted";
@@ -10,52 +8,86 @@ const MAC_LENGTH = 32;
const MIN_DATA_LENGTH = 1;
export class EncArrayBuffer implements Encrypted {
readonly encryptionType: EncryptionType = null;
readonly dataBytes: Uint8Array = null;
readonly ivBytes: Uint8Array = null;
readonly macBytes: Uint8Array = null;
readonly encryptionType: EncryptionType;
readonly dataBytes: Uint8Array;
readonly ivBytes: Uint8Array;
readonly macBytes: Uint8Array | null = null;
private static readonly DecryptionError = new Error(
"Error parsing encrypted ArrayBuffer: data is corrupted or has an invalid format.",
);
constructor(readonly buffer: Uint8Array) {
const encBytes = buffer;
const encType = encBytes[0];
if (buffer == null) {
throw new Error("EncArrayBuffer initialized with null buffer.");
}
switch (encType) {
this.encryptionType = this.buffer[0];
switch (this.encryptionType) {
case EncryptionType.AesCbc128_HmacSha256_B64:
case EncryptionType.AesCbc256_HmacSha256_B64: {
const minimumLength = ENC_TYPE_LENGTH + IV_LENGTH + MAC_LENGTH + MIN_DATA_LENGTH;
if (encBytes.length < minimumLength) {
this.throwDecryptionError();
if (this.buffer.length < minimumLength) {
throw EncArrayBuffer.DecryptionError;
}
this.ivBytes = encBytes.slice(ENC_TYPE_LENGTH, ENC_TYPE_LENGTH + IV_LENGTH);
this.macBytes = encBytes.slice(
this.ivBytes = this.buffer.slice(ENC_TYPE_LENGTH, ENC_TYPE_LENGTH + IV_LENGTH);
this.macBytes = this.buffer.slice(
ENC_TYPE_LENGTH + IV_LENGTH,
ENC_TYPE_LENGTH + IV_LENGTH + MAC_LENGTH,
);
this.dataBytes = encBytes.slice(ENC_TYPE_LENGTH + IV_LENGTH + MAC_LENGTH);
this.dataBytes = this.buffer.slice(ENC_TYPE_LENGTH + IV_LENGTH + MAC_LENGTH);
break;
}
case EncryptionType.AesCbc256_B64: {
const minimumLength = ENC_TYPE_LENGTH + IV_LENGTH + MIN_DATA_LENGTH;
if (encBytes.length < minimumLength) {
this.throwDecryptionError();
if (this.buffer.length < minimumLength) {
throw EncArrayBuffer.DecryptionError;
}
this.ivBytes = encBytes.slice(ENC_TYPE_LENGTH, ENC_TYPE_LENGTH + IV_LENGTH);
this.dataBytes = encBytes.slice(ENC_TYPE_LENGTH + IV_LENGTH);
this.ivBytes = this.buffer.slice(ENC_TYPE_LENGTH, ENC_TYPE_LENGTH + IV_LENGTH);
this.dataBytes = this.buffer.slice(ENC_TYPE_LENGTH + IV_LENGTH);
break;
}
default:
this.throwDecryptionError();
throw EncArrayBuffer.DecryptionError;
}
this.encryptionType = encType;
}
private throwDecryptionError() {
throw new Error(
"Error parsing encrypted ArrayBuffer: data is corrupted or has an invalid format.",
);
static fromParts(
encryptionType: EncryptionType,
iv: Uint8Array,
data: Uint8Array,
mac: Uint8Array | undefined | null,
) {
if (encryptionType == null || iv == null || data == null) {
throw new Error("encryptionType, iv, and data must be provided");
}
switch (encryptionType) {
case EncryptionType.AesCbc256_B64:
case EncryptionType.AesCbc128_HmacSha256_B64:
case EncryptionType.AesCbc256_HmacSha256_B64:
EncArrayBuffer.validateIvLength(iv);
EncArrayBuffer.validateMacLength(encryptionType, mac);
break;
default:
throw new Error(`Unknown EncryptionType ${encryptionType} for EncArrayBuffer.fromParts`);
}
let macLen = 0;
if (mac != null) {
macLen = mac.length;
}
const bytes = new Uint8Array(1 + iv.byteLength + macLen + data.byteLength);
bytes.set([encryptionType], 0);
bytes.set(iv, 1);
if (mac != null) {
bytes.set(mac, 1 + iv.byteLength);
}
bytes.set(data, 1 + iv.byteLength + macLen);
return new EncArrayBuffer(bytes);
}
static async fromResponse(response: {
@@ -72,4 +104,28 @@ export class EncArrayBuffer implements Encrypted {
const buffer = Utils.fromB64ToArray(b64);
return new EncArrayBuffer(buffer);
}
static validateIvLength(iv: Uint8Array) {
if (iv == null || iv.length !== IV_LENGTH) {
throw new Error("Invalid IV length");
}
}
static validateMacLength(encType: EncryptionType, mac: Uint8Array | null | undefined) {
switch (encType) {
case EncryptionType.AesCbc256_B64:
if (mac != null) {
throw new Error("mac must not be provided for AesCbc256_B64");
}
break;
case EncryptionType.AesCbc128_HmacSha256_B64:
case EncryptionType.AesCbc256_HmacSha256_B64:
if (mac == null || mac.length !== MAC_LENGTH) {
throw new Error("Invalid MAC length");
}
break;
default:
throw new Error("Invalid encryption type and mac combination");
}
}
}

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, Opaque } from "type-fest";
import { EncryptService } from "../../../key-management/crypto/abstractions/encrypt.service";
@@ -17,7 +15,7 @@ export class EncString implements Encrypted {
decryptedValue?: string;
data?: string;
iv?: string;
mac?: string;
mac: string | undefined | null;
constructor(
encryptedStringOrType: string | EncryptionType,
@@ -32,15 +30,15 @@ export class EncString implements Encrypted {
}
}
get ivBytes(): Uint8Array {
get ivBytes(): Uint8Array | null {
return this.iv == null ? null : Utils.fromB64ToArray(this.iv);
}
get macBytes(): Uint8Array {
get macBytes(): Uint8Array | null {
return this.mac == null ? null : Utils.fromB64ToArray(this.mac);
}
get dataBytes(): Uint8Array {
get dataBytes(): Uint8Array | null {
return this.data == null ? null : Utils.fromB64ToArray(this.data);
}
@@ -48,7 +46,7 @@ export class EncString implements Encrypted {
return this.encryptedString as string;
}
static fromJSON(obj: Jsonify<EncString>): EncString {
static fromJSON(obj: Jsonify<EncString>): EncString | null {
if (obj == null) {
return null;
}
@@ -56,7 +54,12 @@ export class EncString implements Encrypted {
return new EncString(obj);
}
private initFromData(encType: EncryptionType, data: string, iv: string, mac: string) {
private initFromData(
encType: EncryptionType,
data: string,
iv: string | undefined,
mac: string | undefined,
) {
if (iv != null) {
this.encryptedString = (encType + "." + iv + "|" + data) as EncryptedString;
} else {
@@ -119,15 +122,13 @@ export class EncString implements Encrypted {
} {
const headerPieces = encryptedString.split(".");
let encType: EncryptionType;
let encPieces: string[] = null;
let encPieces: string[];
if (headerPieces.length === 2) {
try {
encType = parseInt(headerPieces[0], null);
encType = parseInt(headerPieces[0]);
encPieces = headerPieces[1].split("|");
// FIXME: Remove when updating file. Eslint update
// eslint-disable-next-line @typescript-eslint/no-unused-vars
} catch (e) {
} catch {
return { encType: NaN, encPieces: [] };
}
} else {
@@ -160,7 +161,7 @@ export class EncString implements Encrypted {
async decrypt(
orgId: string | null,
key: SymmetricCryptoKey = null,
key: SymmetricCryptoKey | null = null,
context?: string,
): Promise<string> {
if (this.decryptedValue != null) {
@@ -219,7 +220,7 @@ export class EncString implements Encrypted {
return this.decryptedValue;
}
private async getKeyForDecryption(orgId: string) {
private async getKeyForDecryption(orgId: string | null) {
const keyService = Utils.getContainerService().getKeyService();
return orgId != null
? await keyService.getOrgKey(orgId)

View File

@@ -361,8 +361,6 @@ describe("ConfigService", () => {
const configs = await firstValueFrom(sut.serverConfig$.pipe(bufferCount(2)));
await jest.runOnlyPendingTimersAsync();
expect(configs[0].gitHash).toBe("existing-data");
expect(configs[1].gitHash).toBe("slow-response");
});

View File

@@ -10,6 +10,7 @@ import {
of,
shareReplay,
Subject,
Subscription,
switchMap,
tap,
} from "rxjs";
@@ -17,14 +18,14 @@ import { SemVer } from "semver";
import { AuthService } from "../../../auth/abstractions/auth.service";
import { AuthenticationStatus } from "../../../auth/enums/authentication-status";
import {
DefaultFeatureFlagValue,
FeatureFlag,
FeatureFlagValueType,
} from "../../../enums/feature-flag.enum";
import { FeatureFlag, getFeatureFlagValue } from "../../../enums/feature-flag.enum";
import { UserId } from "../../../types/guid";
import { ConfigApiServiceAbstraction } from "../../abstractions/config/config-api.service.abstraction";
import { ConfigService } from "../../abstractions/config/config.service";
import {
ConfigCallback,
ConfigService,
OnServerConfigChange,
} from "../../abstractions/config/config.service";
import { ServerConfig } from "../../abstractions/config/server-config";
import { Environment, EnvironmentService, Region } from "../../abstractions/environment.service";
import { LogService } from "../../abstractions/log.service";
@@ -57,6 +58,7 @@ export const GLOBAL_SERVER_CONFIGURATIONS = KeyDefinition.record<ServerConfig, A
// FIXME: currently we are limited to api requests for active users. Update to accept a UserId and APIUrl once ApiService supports it.
export class DefaultConfigService implements ConfigService {
private failedFetchFallbackSubject = new Subject<ServerConfig>();
private callbacks: ConfigCallback[] = [];
serverConfig$: Observable<ServerConfig>;
@@ -123,26 +125,13 @@ export class DefaultConfigService implements ConfigService {
}
getFeatureFlag$<Flag extends FeatureFlag>(key: Flag) {
return this.serverConfig$.pipe(
map((serverConfig) => this.getFeatureFlagValue(serverConfig, key)),
);
}
private getFeatureFlagValue<Flag extends FeatureFlag>(
serverConfig: ServerConfig | null,
flag: Flag,
) {
if (serverConfig?.featureStates == null || serverConfig.featureStates[flag] == null) {
return DefaultFeatureFlagValue[flag];
}
return serverConfig.featureStates[flag] as FeatureFlagValueType<Flag>;
return this.serverConfig$.pipe(map((serverConfig) => getFeatureFlagValue(serverConfig, key)));
}
userCachedFeatureFlag$<Flag extends FeatureFlag>(key: Flag, userId: UserId) {
return this.stateProvider
.getUser(userId, USER_SERVER_CONFIG)
.state$.pipe(map((config) => this.getFeatureFlagValue(config, key)));
.state$.pipe(map((config) => getFeatureFlagValue(config, key)));
}
async getFeatureFlag<Flag extends FeatureFlag>(key: Flag) {
@@ -166,6 +155,12 @@ export class DefaultConfigService implements ConfigService {
await firstValueFrom(this.serverConfig$);
}
broadcastConfigChangesTo(...listeners: OnServerConfigChange[]): Subscription {
return this.serverConfig$.subscribe((config) =>
listeners.forEach((listener) => listener.onServerConfigChange(config)),
);
}
private olderThanRetrievalInterval(date: Date) {
return new Date().getTime() - date.getTime() > RETRIEVAL_INTERVAL;
}

View File

@@ -134,7 +134,6 @@ export class StateService<
}
async addAccount(account: TAccount) {
await this.environmentService.seedUserEnvironment(account.profile.userId as UserId);
await this.updateState(async (state) => {
state.accounts[account.profile.userId] = account;
return state;

View File

@@ -1,6 +1,6 @@
import { Observable } from "rxjs";
import { OrganizationId, UserId } from "@bitwarden/common/types/guid";
import { OrganizationId, UserId } from "../types/guid";
/** error emitted when the `SingleUserDependency` changes Ids */
export type UserChangedError = {

View File

@@ -6,6 +6,9 @@ import { DefaultSemanticLogger } from "./default-semantic-logger";
import { DisabledSemanticLogger } from "./disabled-semantic-logger";
import { SemanticLogger } from "./semantic-logger.abstraction";
/** A type for injection of a log provider */
export type LogProvider = <Context>(context: Jsonify<Context>) => SemanticLogger;
/** Instantiates a semantic logger that emits nothing when a message
* is logged.
* @param _context a static payload that is cloned when the logger
@@ -25,8 +28,11 @@ export function disabledSemanticLoggerProvider<Context extends object>(
* @param settings specializes how the semantic logger functions.
* If this is omitted, the logger suppresses debug messages.
*/
export function consoleSemanticLoggerProvider(logger: LogService): SemanticLogger {
return new DefaultSemanticLogger(logger, {});
export function consoleSemanticLoggerProvider<Context extends object>(
logger: LogService,
context: Jsonify<Context>,
): SemanticLogger {
return new DefaultSemanticLogger(logger, context);
}
/** Instantiates a semantic logger that emits logs to the console.
@@ -42,7 +48,7 @@ export function ifEnabledSemanticLoggerProvider<Context extends object>(
context: Jsonify<Context>,
) {
if (enable) {
return new DefaultSemanticLogger(logger, context);
return consoleSemanticLoggerProvider(logger, context);
} else {
return disabledSemanticLoggerProvider(context);
}

View File

@@ -12,6 +12,7 @@ export abstract class UserStateSubjectDependencyProvider {
/** Provides local object persistence */
abstract state: StateProvider;
// FIXME: remove `log` and inject the system provider into the USS instead
/** Provides semantic logging */
abstract log: <Context extends object>(_context: Jsonify<Context>) => SemanticLogger;
}

View File

@@ -81,7 +81,7 @@ const DEFAULT_FRAME_SIZE = 32;
export class UserStateSubject<
State extends object,
Secret = State,
Disclosed = never,
Disclosed = Record<string, never>,
Dependencies = null,
>
extends Observable<State>
@@ -243,7 +243,7 @@ export class UserStateSubject<
// `init$` becomes the accumulator for `scan`
init$.pipe(
first(),
map((init) => [init, null] as const),
map((init) => [init, null] as [State, Dependencies]),
),
input$.pipe(
map((constrained) => constrained.state),
@@ -256,7 +256,7 @@ export class UserStateSubject<
if (shouldUpdate) {
// actual update
const next = this.context.nextValue?.(prev, pending, dependencies) ?? pending;
return [next, dependencies];
return [next, dependencies] as const;
} else {
// false update
this.log.debug("shouldUpdate prevented write");

View File

@@ -980,10 +980,14 @@ export class CipherService implements CipherServiceAbstraction {
async upsert(cipher: CipherData | CipherData[]): Promise<Record<CipherId, CipherData>> {
const ciphers = cipher instanceof CipherData ? [cipher] : cipher;
return await this.updateEncryptedCipherState((current) => {
const res = await this.updateEncryptedCipherState((current) => {
ciphers.forEach((c) => (current[c.id as CipherId] = c));
return current;
});
// Some state storage providers (e.g. Electron) don't update the state immediately, wait for next tick
// Otherwise, subscribers to cipherViews$ can get stale data
await new Promise((resolve) => setTimeout(resolve, 0));
return res;
}
async replace(ciphers: { [id: string]: CipherData }, userId: UserId): Promise<any> {
@@ -1000,13 +1004,16 @@ export class CipherService implements CipherServiceAbstraction {
userId: UserId = null,
): Promise<Record<CipherId, CipherData>> {
userId ||= await firstValueFrom(this.stateProvider.activeUserId$);
await this.clearDecryptedCiphersState(userId);
await this.clearCache(userId);
const updatedCiphers = await this.stateProvider
.getUser(userId, ENCRYPTED_CIPHERS)
.update((current) => {
const result = update(current ?? {});
return result;
});
// Some state storage providers (e.g. Electron) don't update the state immediately, wait for next tick
// Otherwise, subscribers to cipherViews$ can get stale data
await new Promise((resolve) => setTimeout(resolve, 0));
return updatedCiphers;
}