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:
@@ -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>;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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>) => {
|
||||
|
||||
@@ -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.");
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
15
libs/common/src/platform/enums/encryption-type.enum.spec.ts
Normal file
15
libs/common/src/platform/enums/encryption-type.enum.spec.ts
Normal 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);
|
||||
});
|
||||
});
|
||||
@@ -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];
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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`,
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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");
|
||||
});
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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 = {
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user