1
0
mirror of https://github.com/bitwarden/browser synced 2026-02-16 16:59:30 +00:00

Merge branch 'main' into km/test-drop-encrypted-object

This commit is contained in:
Bernd Schoolmann
2025-08-18 11:39:47 +02:00
committed by GitHub
595 changed files with 17765 additions and 11351 deletions

View File

@@ -3,12 +3,13 @@
import { Jsonify } from "type-fest";
import { ProductTierType } from "../../../billing/enums";
import { OrganizationId } from "../../../types/guid";
import { OrganizationUserStatusType, OrganizationUserType, ProviderType } from "../../enums";
import { PermissionsApi } from "../api/permissions.api";
import { OrganizationData } from "../data/organization.data";
export class Organization {
id: string;
id: OrganizationId;
name: string;
status: OrganizationUserStatusType;
@@ -99,7 +100,7 @@ export class Organization {
return;
}
this.id = obj.id;
this.id = obj.id as OrganizationId;
this.name = obj.name;
this.status = obj.status;
this.type = obj.type;

View File

@@ -2,14 +2,14 @@
// @ts-strict-ignore
import { ListResponse } from "../../../models/response/list.response";
import Domain from "../../../platform/models/domain/domain-base";
import { PolicyId } from "../../../types/guid";
import { OrganizationId, PolicyId } from "../../../types/guid";
import { PolicyType } from "../../enums";
import { PolicyData } from "../data/policy.data";
import { PolicyResponse } from "../response/policy.response";
export class Policy extends Domain {
id: PolicyId;
organizationId: string;
organizationId: OrganizationId;
type: PolicyType;
data: any;
@@ -26,7 +26,7 @@ export class Policy extends Domain {
}
this.id = obj.id;
this.organizationId = obj.organizationId;
this.organizationId = obj.organizationId as OrganizationId;
this.type = obj.type;
this.data = obj.data;
this.enabled = obj.enabled;

View File

@@ -1,9 +1,7 @@
// FIXME: Update this file to be type safe and remove this and next line
// @ts-strict-ignore
import { PolicyType } from "../../enums";
export class PolicyRequest {
export type PolicyRequest = {
type: PolicyType;
enabled: boolean;
data: any;
}
};

View File

@@ -1,5 +1,4 @@
import { ClientType } from "../../../../enums";
import { Utils } from "../../../../platform/misc/utils";
import { DeviceRequest } from "./device.request";
import { TokenTwoFactorRequest } from "./token-two-factor.request";
@@ -30,10 +29,6 @@ export class PasswordTokenRequest extends TokenRequest {
return obj;
}
alterIdentityTokenHeaders(headers: Headers) {
headers.set("Auth-Email", Utils.fromUtf8ToUrlB64(this.email));
}
static fromJSON(json: any) {
return Object.assign(Object.create(PasswordTokenRequest.prototype), json, {
device: json.device ? DeviceRequest.fromJSON(json.device) : undefined,

View File

@@ -14,10 +14,6 @@ export abstract class TokenRequest {
this.device = device != null ? device : null;
}
alterIdentityTokenHeaders(headers: Headers) {
// Implemented in subclass if required
}
setTwoFactor(twoFactor: TokenTwoFactorRequest | undefined) {
this.twoFactor = twoFactor;
}

View File

@@ -47,13 +47,10 @@ export enum FeatureFlag {
EventBasedOrganizationIntegrations = "event-based-organization-integrations",
/* Vault */
PM8851_BrowserOnboardingNudge = "pm-8851-browser-onboarding-nudge",
PM9111ExtensionPersistAddEditForm = "pm-9111-extension-persist-add-edit-form",
PM19941MigrateCipherDomainToSdk = "pm-19941-migrate-cipher-domain-to-sdk",
PM22134SdkCipherListView = "pm-22134-sdk-cipher-list-view",
PM22136_SdkCipherEncryption = "pm-22136-sdk-cipher-encryption",
CipherKeyEncryption = "cipher-key-encryption",
EndUserNotifications = "pm-10609-end-user-notifications",
RemoveCardItemTypePolicy = "pm-16442-remove-card-item-type-policy",
PM19315EndUserActivationMvp = "pm-19315-end-user-activation-mvp",
@@ -94,10 +91,7 @@ export const DefaultFeatureFlagValue = {
[FeatureFlag.EventBasedOrganizationIntegrations]: FALSE,
/* Vault */
[FeatureFlag.PM8851_BrowserOnboardingNudge]: FALSE,
[FeatureFlag.PM9111ExtensionPersistAddEditForm]: FALSE,
[FeatureFlag.CipherKeyEncryption]: FALSE,
[FeatureFlag.EndUserNotifications]: FALSE,
[FeatureFlag.PM19941MigrateCipherDomainToSdk]: FALSE,
[FeatureFlag.RemoveCardItemTypePolicy]: FALSE,
[FeatureFlag.PM22134SdkCipherListView]: FALSE,

View File

@@ -0,0 +1,2 @@
export { KeyGenerationService } from "./key-generation/key-generation.service";
export { DefaultKeyGenerationService } from "./key-generation/default-key-generation.service";

View File

@@ -0,0 +1,94 @@
// FIXME: Update this file to be type safe and remove this and next line
// @ts-strict-ignore
// This import has been flagged as unallowed for this class. It may be involved in a circular dependency loop.
// eslint-disable-next-line no-restricted-imports
import { KdfConfig } from "@bitwarden/key-management";
import { PureCrypto } from "@bitwarden/sdk-internal";
import { SdkLoadService } from "../../../platform/abstractions/sdk/sdk-load.service";
import { EncryptionType } from "../../../platform/enums";
import { Utils } from "../../../platform/misc/utils";
import { SymmetricCryptoKey } from "../../../platform/models/domain/symmetric-crypto-key";
import { CsprngArray } from "../../../types/csprng";
import { CryptoFunctionService } from "../abstractions/crypto-function.service";
import { KeyGenerationService } from "./key-generation.service";
export class DefaultKeyGenerationService implements KeyGenerationService {
constructor(private cryptoFunctionService: CryptoFunctionService) {}
async createKey(bitLength: 256 | 512): Promise<SymmetricCryptoKey> {
const key = await this.cryptoFunctionService.aesGenerateKey(bitLength);
return new SymmetricCryptoKey(key);
}
async createKeyWithPurpose(
bitLength: 128 | 192 | 256 | 512,
purpose: string,
salt?: string,
): Promise<{ salt: string; material: CsprngArray; derivedKey: SymmetricCryptoKey }> {
if (salt == null) {
const bytes = await this.cryptoFunctionService.randomBytes(32);
salt = Utils.fromBufferToUtf8(bytes);
}
const material = await this.cryptoFunctionService.aesGenerateKey(bitLength);
const key = await this.cryptoFunctionService.hkdf(material, salt, purpose, 64, "sha256");
return { salt, material, derivedKey: new SymmetricCryptoKey(key) };
}
async deriveKeyFromMaterial(
material: CsprngArray,
salt: string,
purpose: string,
): Promise<SymmetricCryptoKey> {
const key = await this.cryptoFunctionService.hkdf(material, salt, purpose, 64, "sha256");
return new SymmetricCryptoKey(key);
}
async deriveKeyFromPassword(
password: string | Uint8Array,
salt: string | Uint8Array,
kdfConfig: KdfConfig,
): Promise<SymmetricCryptoKey> {
if (typeof password === "string") {
password = new TextEncoder().encode(password);
}
if (typeof salt === "string") {
salt = new TextEncoder().encode(salt);
}
await SdkLoadService.Ready;
return new SymmetricCryptoKey(
PureCrypto.derive_kdf_material(password, salt, kdfConfig.toSdkConfig()),
);
}
async stretchKey(key: SymmetricCryptoKey): Promise<SymmetricCryptoKey> {
// The key to be stretched is actually usually the output of a KDF, and not actually meant for AesCbc256_B64 encryption,
// but has the same key length. Only 256-bit key materials should be stretched.
if (key.inner().type != EncryptionType.AesCbc256_B64) {
throw new Error("Key passed into stretchKey is not a 256-bit key.");
}
const newKey = new Uint8Array(64);
// Master key and pin key are always 32 bytes
const encKey = await this.cryptoFunctionService.hkdfExpand(
key.inner().encryptionKey,
"enc",
32,
"sha256",
);
const macKey = await this.cryptoFunctionService.hkdfExpand(
key.inner().encryptionKey,
"mac",
32,
"sha256",
);
newKey.set(new Uint8Array(encKey));
newKey.set(new Uint8Array(macKey), 32);
return new SymmetricCryptoKey(newKey);
}
}

View File

@@ -4,21 +4,21 @@ import { mock } from "jest-mock-extended";
// eslint-disable-next-line no-restricted-imports
import { PBKDF2KdfConfig, Argon2KdfConfig } from "@bitwarden/key-management";
import { CryptoFunctionService } from "../../key-management/crypto/abstractions/crypto-function.service";
import { CsprngArray } from "../../types/csprng";
import { SdkLoadService } from "../abstractions/sdk/sdk-load.service";
import { EncryptionType } from "../enums";
import { SymmetricCryptoKey } from "../models/domain/symmetric-crypto-key";
import { SdkLoadService } from "../../../platform/abstractions/sdk/sdk-load.service";
import { EncryptionType } from "../../../platform/enums";
import { SymmetricCryptoKey } from "../../../platform/models/domain/symmetric-crypto-key";
import { CsprngArray } from "../../../types/csprng";
import { CryptoFunctionService } from "../abstractions/crypto-function.service";
import { KeyGenerationService } from "./key-generation.service";
import { DefaultKeyGenerationService } from "./default-key-generation.service";
describe("KeyGenerationService", () => {
let sut: KeyGenerationService;
let sut: DefaultKeyGenerationService;
const cryptoFunctionService = mock<CryptoFunctionService>();
beforeEach(() => {
sut = new KeyGenerationService(cryptoFunctionService);
sut = new DefaultKeyGenerationService(cryptoFunctionService);
});
describe("createKey", () => {

View File

@@ -0,0 +1,90 @@
// This import has been flagged as unallowed for this class. It may be involved in a circular dependency loop.
// eslint-disable-next-line no-restricted-imports
import { KdfConfig } from "@bitwarden/key-management";
import { SymmetricCryptoKey } from "../../../platform/models/domain/symmetric-crypto-key";
import { CsprngArray } from "../../../types/csprng";
/**
* @deprecated This is a low-level cryptographic service. New functionality should not be built
* on top of it, and instead should be built in the sdk.
*/
export abstract class KeyGenerationService {
/**
* Generates a key of the given length suitable for use in AES encryption
*
* @deprecated WARNING: DO NOT USE THIS FOR NEW CODE. Direct generation and handling of keys should only be done in the SDK,
* as memory safety cannot be ensured in a JS context.
*
* @param bitLength Length of key.
* 256 bits = 32 bytes
* 512 bits = 64 bytes
* @returns Generated key.
*/
abstract createKey(bitLength: 256 | 512): Promise<SymmetricCryptoKey>;
/**
* Generates key material from CSPRNG and derives a 64 byte key from it.
* Uses HKDF, see {@link https://datatracker.ietf.org/doc/html/rfc5869 RFC 5869}
* for details.
*
* @deprecated HAZMAT WARNING: DO NOT USE THIS FOR NEW CODE. This is a low-level cryptographic function.
* New functionality should not be built on top of it, and instead should be built in the sdk.
*
* @param bitLength Length of key material.
* @param purpose Purpose for the key derivation function.
* Different purposes results in different keys, even with the same material.
* @param salt Optional. If not provided will be generated from CSPRNG.
* @returns An object containing the salt, key material, and derived key.
*/
abstract createKeyWithPurpose(
bitLength: 128 | 192 | 256 | 512,
purpose: string,
salt?: string,
): Promise<{ salt: string; material: CsprngArray; derivedKey: SymmetricCryptoKey }>;
/**
* Derives a 64 byte key from key material.
*
* @deprecated HAZMAT WARNING: DO NOT USE THIS FOR NEW CODE. This is a low-level cryptographic function.
* New functionality should not be built on top of it, and instead should be built in the sdk.
*
* @remark The key material should be generated from {@link createKey}, or {@link createKeyWithPurpose}.
* Uses HKDF, see {@link https://datatracker.ietf.org/doc/html/rfc5869 RFC 5869} for details.
* @param material key material.
* @param salt Salt for the key derivation function.
* @param purpose Purpose for the key derivation function.
* Different purposes results in different keys, even with the same material.
* @returns 64 byte derived key.
*/
abstract deriveKeyFromMaterial(
material: CsprngArray,
salt: string,
purpose: string,
): Promise<SymmetricCryptoKey>;
/**
* Derives a 32 byte key from a password using a key derivation function.
*
* @deprecated HAZMAT WARNING: DO NOT USE THIS FOR NEW CODE. This is a low-level cryptographic function.
* New functionality should not be built on top of it, and instead should be built in the sdk.
*
* @param password Password to derive the key from.
* @param salt Salt for the key derivation function.
* @param kdfConfig Configuration for the key derivation function.
* @returns 32 byte derived key.
*/
abstract deriveKeyFromPassword(
password: string | Uint8Array,
salt: string | Uint8Array,
kdfConfig: KdfConfig,
): Promise<SymmetricCryptoKey>;
/**
* Derives a 64 byte key from a 32 byte key using a key derivation function.
*
* @deprecated HAZMAT WARNING: DO NOT USE THIS FOR NEW CODE. This is a low-level cryptographic function.
* New functionality should not be built on top of it, and instead should be built in the sdk.
*
* @param key 32 byte key.
* @returns 64 byte derived key.
*/
abstract stretchKey(key: SymmetricCryptoKey): Promise<SymmetricCryptoKey>;
}

View File

@@ -5,13 +5,12 @@ import { Jsonify } from "type-fest";
import { EncString as SdkEncString } from "@bitwarden/sdk-internal";
import { EncryptionType, EXPECTED_NUM_PARTS_BY_ENCRYPTION_TYPE } from "../../../platform/enums";
import { Encrypted } from "../../../platform/interfaces/encrypted";
import { Utils } from "../../../platform/misc/utils";
import { SymmetricCryptoKey } from "../../../platform/models/domain/symmetric-crypto-key";
export const DECRYPT_ERROR = "[error: cannot decrypt]";
export class EncString implements Encrypted {
export class EncString {
encryptedString?: SdkEncString;
encryptionType?: EncryptionType;
decryptedValue?: string;

View File

@@ -20,7 +20,6 @@ import {
import { AppIdService } from "../../../platform/abstractions/app-id.service";
import { ConfigService } from "../../../platform/abstractions/config/config.service";
import { I18nService } from "../../../platform/abstractions/i18n.service";
import { KeyGenerationService } from "../../../platform/abstractions/key-generation.service";
import { LogService } from "../../../platform/abstractions/log.service";
import { PlatformUtilsService } from "../../../platform/abstractions/platform-utils.service";
import { AbstractStorageService } from "../../../platform/abstractions/storage.service";
@@ -30,6 +29,7 @@ import { SymmetricCryptoKey } from "../../../platform/models/domain/symmetric-cr
import { DEVICE_TRUST_DISK_LOCAL, StateProvider, UserKeyDefinition } from "../../../platform/state";
import { UserId } from "../../../types/guid";
import { UserKey, DeviceKey } from "../../../types/key";
import { KeyGenerationService } from "../../crypto";
import { CryptoFunctionService } from "../../crypto/abstractions/crypto-function.service";
import { EncryptService } from "../../crypto/abstractions/encrypt.service";
import { EncString } from "../../crypto/models/enc-string";

View File

@@ -25,7 +25,6 @@ import { DeviceType } from "../../../enums";
import { AppIdService } from "../../../platform/abstractions/app-id.service";
import { ConfigService } from "../../../platform/abstractions/config/config.service";
import { I18nService } from "../../../platform/abstractions/i18n.service";
import { KeyGenerationService } from "../../../platform/abstractions/key-generation.service";
import { LogService } from "../../../platform/abstractions/log.service";
import { PlatformUtilsService } from "../../../platform/abstractions/platform-utils.service";
import { AbstractStorageService } from "../../../platform/abstractions/storage.service";
@@ -37,6 +36,7 @@ import { SymmetricCryptoKey } from "../../../platform/models/domain/symmetric-cr
import { CsprngArray } from "../../../types/csprng";
import { UserId } from "../../../types/guid";
import { DeviceKey, UserKey } from "../../../types/key";
import { KeyGenerationService } from "../../crypto";
import { CryptoFunctionService } from "../../crypto/abstractions/crypto-function.service";
import { EncryptService } from "../../crypto/abstractions/encrypt.service";
import { EncString } from "../../crypto/models/enc-string";

View File

@@ -18,9 +18,9 @@ import { TokenService } from "../../../auth/services/token.service";
import { LogService } from "../../../platform/abstractions/log.service";
import { Utils } from "../../../platform/misc/utils";
import { SymmetricCryptoKey } from "../../../platform/models/domain/symmetric-crypto-key";
import { KeyGenerationService } from "../../../platform/services/key-generation.service";
import { OrganizationId, UserId } from "../../../types/guid";
import { MasterKey, UserKey } from "../../../types/key";
import { KeyGenerationService } from "../../crypto";
import { EncString } from "../../crypto/models/enc-string";
import { FakeMasterPasswordService } from "../../master-password/services/fake-master-password.service";
import { KeyConnectorUserKeyRequest } from "../models/key-connector-user-key.request";

View File

@@ -23,13 +23,13 @@ import { Organization } from "../../../admin-console/models/domain/organization"
import { TokenService } from "../../../auth/abstractions/token.service";
import { IdentityTokenResponse } from "../../../auth/models/response/identity-token.response";
import { KeysRequest } from "../../../models/request/keys.request";
import { KeyGenerationService } from "../../../platform/abstractions/key-generation.service";
import { LogService } from "../../../platform/abstractions/log.service";
import { Utils } from "../../../platform/misc/utils";
import { SymmetricCryptoKey } from "../../../platform/models/domain/symmetric-crypto-key";
import { KEY_CONNECTOR_DISK, StateProvider, UserKeyDefinition } from "../../../platform/state";
import { UserId } from "../../../types/guid";
import { MasterKey } from "../../../types/key";
import { KeyGenerationService } from "../../crypto";
import { InternalMasterPasswordServiceAbstraction } from "../../master-password/abstractions/master-password.service.abstraction";
import { KeyConnectorService as KeyConnectorServiceAbstraction } from "../abstractions/key-connector.service";
import { KeyConnectorUserKeyRequest } from "../models/key-connector-user-key.request";

View File

@@ -13,13 +13,13 @@ import {
mockAccountServiceWith,
} from "../../../../spec";
import { ForceSetPasswordReason } from "../../../auth/models/domain/force-set-password-reason";
import { KeyGenerationService } from "../../../platform/abstractions/key-generation.service";
import { LogService } from "../../../platform/abstractions/log.service";
import { StateService } from "../../../platform/abstractions/state.service";
import { SymmetricCryptoKey } from "../../../platform/models/domain/symmetric-crypto-key";
import { StateProvider } from "../../../platform/state";
import { UserId } from "../../../types/guid";
import { MasterKey, UserKey } from "../../../types/key";
import { KeyGenerationService } from "../../crypto";
import { CryptoFunctionService } from "../../crypto/abstractions/crypto-function.service";
import { EncryptService } from "../../crypto/abstractions/encrypt.service";
import { EncString } from "../../crypto/models/enc-string";

View File

@@ -11,7 +11,6 @@ import { KdfConfig } from "@bitwarden/key-management";
import { PureCrypto } from "@bitwarden/sdk-internal";
import { ForceSetPasswordReason } from "../../../auth/models/domain/force-set-password-reason";
import { KeyGenerationService } from "../../../platform/abstractions/key-generation.service";
import { LogService } from "../../../platform/abstractions/log.service";
import { StateService } from "../../../platform/abstractions/state.service";
import { EncryptionType } from "../../../platform/enums";
@@ -24,6 +23,7 @@ import {
} from "../../../platform/state";
import { UserId } from "../../../types/guid";
import { MasterKey, UserKey } from "../../../types/key";
import { KeyGenerationService } from "../../crypto";
import { CryptoFunctionService } from "../../crypto/abstractions/crypto-function.service";
import { EncryptService } from "../../crypto/abstractions/encrypt.service";
import { EncryptedString, EncString } from "../../crypto/models/enc-string";

View File

@@ -9,11 +9,11 @@ import { AccountService } from "../../auth/abstractions/account.service";
import { CryptoFunctionService } from "../../key-management/crypto/abstractions/crypto-function.service";
import { EncryptService } from "../../key-management/crypto/abstractions/encrypt.service";
import { EncString, EncryptedString } from "../../key-management/crypto/models/enc-string";
import { KeyGenerationService } from "../../platform/abstractions/key-generation.service";
import { LogService } from "../../platform/abstractions/log.service";
import { PIN_DISK, PIN_MEMORY, StateProvider, UserKeyDefinition } from "../../platform/state";
import { UserId } from "../../types/guid";
import { PinKey, UserKey } from "../../types/key";
import { KeyGenerationService } from "../crypto";
import { PinServiceAbstraction } from "./pin.service.abstraction";

View File

@@ -4,12 +4,12 @@ import { mock } from "jest-mock-extended";
import { DEFAULT_KDF_CONFIG, KdfConfigService } from "@bitwarden/key-management";
import { FakeAccountService, FakeStateProvider, mockAccountServiceWith } from "../../../spec";
import { KeyGenerationService } from "../../platform/abstractions/key-generation.service";
import { LogService } from "../../platform/abstractions/log.service";
import { Utils } from "../../platform/misc/utils";
import { SymmetricCryptoKey } from "../../platform/models/domain/symmetric-crypto-key";
import { UserId } from "../../types/guid";
import { PinKey, UserKey } from "../../types/key";
import { KeyGenerationService } from "../crypto";
import { CryptoFunctionService } from "../crypto/abstractions/crypto-function.service";
import { EncryptService } from "../crypto/abstractions/encrypt.service";
import { EncString } from "../crypto/models/enc-string";

View File

@@ -3,18 +3,18 @@
// This import has been flagged as unallowed for this class. It may be involved in a circular dependency loop.
// eslint-disable-next-line no-restricted-imports
import { Collection as CollectionDomain, CollectionView } from "@bitwarden/admin-console/common";
import { CollectionId } from "@bitwarden/common/types/guid";
import { CollectionExport } from "./collection.export";
export class CollectionWithIdExport extends CollectionExport {
id: string;
id: CollectionId;
static toView(req: CollectionWithIdExport, view = new CollectionView()) {
view.id = req.id;
return super.toView(req, view);
static toView(req: CollectionWithIdExport) {
return super.toView(req, req.id);
}
static toDomain(req: CollectionWithIdExport, domain = new CollectionDomain()) {
static toDomain(req: CollectionWithIdExport, domain: CollectionDomain) {
domain.id = req.id;
return super.toDomain(req, domain);
}

View File

@@ -5,28 +5,30 @@
import { Collection as CollectionDomain, CollectionView } from "@bitwarden/admin-console/common";
import { EncString } from "../../key-management/crypto/models/enc-string";
import { CollectionId, emptyGuid, OrganizationId } from "../../types/guid";
import { safeGetString } from "./utils";
export class CollectionExport {
static template(): CollectionExport {
const req = new CollectionExport();
req.organizationId = "00000000-0000-0000-0000-000000000000";
req.organizationId = emptyGuid as OrganizationId;
req.name = "Collection name";
req.externalId = null;
return req;
}
static toView(req: CollectionExport, view = new CollectionView()) {
view.name = req.name;
static toView(req: CollectionExport, id: CollectionId) {
const view = new CollectionView({
name: req.name,
organizationId: req.organizationId,
id,
});
view.externalId = req.externalId;
if (view.organizationId == null) {
view.organizationId = req.organizationId;
}
return view;
}
static toDomain(req: CollectionExport, domain = new CollectionDomain()) {
static toDomain(req: CollectionExport, domain: CollectionDomain) {
domain.name = req.name != null ? new EncString(req.name) : null;
domain.externalId = req.externalId;
if (domain.organizationId == null) {
@@ -35,7 +37,7 @@ export class CollectionExport {
return domain;
}
organizationId: string;
organizationId: OrganizationId;
name: string;
externalId: string;

View File

@@ -5,5 +5,5 @@ export abstract class ConfigApiServiceAbstraction {
/**
* Fetches the server configuration for the given user. If no user is provided, the configuration will not contain user-specific context.
*/
abstract get(userId: UserId | undefined): Promise<ServerConfigResponse>;
abstract get(userId: UserId | null): Promise<ServerConfigResponse>;
}

View File

@@ -95,6 +95,13 @@ export interface Environment {
*/
export abstract class EnvironmentService {
abstract environment$: Observable<Environment>;
/**
* The environment stored in global state, when a user signs in the state stored here will become
* their user environment.
*/
abstract globalEnvironment$: Observable<Environment>;
abstract cloudWebVaultUrl$: Observable<string>;
/**
@@ -125,12 +132,12 @@ export abstract class EnvironmentService {
* @param userId - The user id to set the cloud web vault app URL for. If null or undefined the global environment is set.
* @param region - The region of the cloud web vault app.
*/
abstract setCloudRegion(userId: UserId, region: Region): Promise<void>;
abstract setCloudRegion(userId: UserId | null, region: Region): Promise<void>;
/**
* Get the environment from state. Useful if you need to get the environment for another user.
*/
abstract getEnvironment$(userId: UserId): Observable<Environment | undefined>;
abstract getEnvironment$(userId: UserId): Observable<Environment>;
/**
* @deprecated Use {@link getEnvironment$} instead.

View File

@@ -1,66 +1,2 @@
// This import has been flagged as unallowed for this class. It may be involved in a circular dependency loop.
// eslint-disable-next-line no-restricted-imports
import { KdfConfig } from "@bitwarden/key-management";
import { CsprngArray } from "../../types/csprng";
import { SymmetricCryptoKey } from "../models/domain/symmetric-crypto-key";
export abstract class KeyGenerationService {
/**
* Generates a key of the given length suitable for use in AES encryption
* @param bitLength Length of key.
* 256 bits = 32 bytes
* 512 bits = 64 bytes
* @returns Generated key.
*/
abstract createKey(bitLength: 256 | 512): Promise<SymmetricCryptoKey>;
/**
* Generates key material from CSPRNG and derives a 64 byte key from it.
* Uses HKDF, see {@link https://datatracker.ietf.org/doc/html/rfc5869 RFC 5869}
* for details.
* @param bitLength Length of key material.
* @param purpose Purpose for the key derivation function.
* Different purposes results in different keys, even with the same material.
* @param salt Optional. If not provided will be generated from CSPRNG.
* @returns An object containing the salt, key material, and derived key.
*/
abstract createKeyWithPurpose(
bitLength: 128 | 192 | 256 | 512,
purpose: string,
salt?: string,
): Promise<{ salt: string; material: CsprngArray; derivedKey: SymmetricCryptoKey }>;
/**
* Derives a 64 byte key from key material.
* @remark The key material should be generated from {@link createKey}, or {@link createKeyWithPurpose}.
* Uses HKDF, see {@link https://datatracker.ietf.org/doc/html/rfc5869 RFC 5869} for details.
* @param material key material.
* @param salt Salt for the key derivation function.
* @param purpose Purpose for the key derivation function.
* Different purposes results in different keys, even with the same material.
* @returns 64 byte derived key.
*/
abstract deriveKeyFromMaterial(
material: CsprngArray,
salt: string,
purpose: string,
): Promise<SymmetricCryptoKey>;
/**
* Derives a 32 byte key from a password using a key derivation function.
* @param password Password to derive the key from.
* @param salt Salt for the key derivation function.
* @param kdfConfig Configuration for the key derivation function.
* @returns 32 byte derived key.
*/
abstract deriveKeyFromPassword(
password: string | Uint8Array,
salt: string | Uint8Array,
kdfConfig: KdfConfig,
): Promise<SymmetricCryptoKey>;
/**
* Derives a 64 byte key from a 32 byte key using a key derivation function.
* @param key 32 byte key.
* @returns 64 byte derived key.
*/
abstract stretchKey(key: SymmetricCryptoKey): Promise<SymmetricCryptoKey>;
}
/** Temporary re-export. This should not be used for new imports */
export { KeyGenerationService } from "../../key-management/crypto/key-generation/key-generation.service";

View File

@@ -1,8 +0,0 @@
import { EncryptionType } from "../enums";
export interface Encrypted {
encryptionType?: EncryptionType;
dataBytes: Uint8Array;
macBytes: Uint8Array;
ivBytes: Uint8Array;
}

View File

@@ -13,8 +13,8 @@ export const getById = <TId, T extends { id: TId }>(id: TId) =>
* @param id The IDs of the objects to return.
* @returns An array containing objects with matching IDs, or an empty array if there are no matching objects.
*/
export const getByIds = <TId, T extends { id: TId | undefined }>(ids: TId[]) => {
const idSet = new Set(ids.filter((id) => id != null));
export const getByIds = <TId, T extends { id: TId }>(ids: TId[]) => {
const idSet = new Set(ids);
return map<T[], T[]>((objects) => {
return objects.filter((o) => o.id && idSet.has(o.id));
});

View File

@@ -13,7 +13,7 @@ export type DecryptedObject<
> = Record<TDecryptedKeys, string> & Omit<TEncryptedObject, TDecryptedKeys>;
// extracts shared keys from the domain and view types
export type EncryptableKeys<D extends Domain, V extends View> = (keyof D &
type EncryptableKeys<D extends Domain, V extends View> = (keyof D &
ConditionalKeys<D, EncString | null>) &
(keyof V & ConditionalKeys<V, string | null>);

View File

@@ -2,14 +2,13 @@
// @ts-strict-ignore
import { Utils } from "../../../platform/misc/utils";
import { EncryptionType } from "../../enums";
import { Encrypted } from "../../interfaces/encrypted";
const ENC_TYPE_LENGTH = 1;
const IV_LENGTH = 16;
const MAC_LENGTH = 32;
const MIN_DATA_LENGTH = 1;
export class EncArrayBuffer implements Encrypted {
export class EncArrayBuffer {
readonly encryptionType: EncryptionType = null;
readonly dataBytes: Uint8Array = null;
readonly ivBytes: Uint8Array = null;

View File

@@ -10,7 +10,7 @@ export class ConfigApiService implements ConfigApiServiceAbstraction {
private tokenService: TokenService,
) {}
async get(userId: UserId | undefined): Promise<ServerConfigResponse> {
async get(userId: UserId | null): Promise<ServerConfigResponse> {
// Authentication adds extra context to config responses, if the user has an access token, we want to use it
// We don't particularly care about ensuring the token is valid and not expired, just that it exists
const authed: boolean =

View File

@@ -10,9 +10,9 @@ import {
FakeGlobalState,
FakeSingleUserState,
FakeStateProvider,
awaitAsync,
mockAccountServiceWith,
} from "../../../../spec";
import { Matrix } from "../../../../spec/matrix";
import { subscribeTo } from "../../../../spec/observable-tracker";
import { AuthService } from "../../../auth/abstractions/auth.service";
import { AuthenticationStatus } from "../../../auth/enums/authentication-status";
@@ -74,7 +74,8 @@ describe("ConfigService", () => {
});
beforeEach(() => {
environmentService.environment$ = environmentSubject;
Matrix.autoMockMethod(environmentService.getEnvironment$, () => environmentSubject);
environmentService.globalEnvironment$ = environmentSubject;
sut = new DefaultConfigService(
configApiService,
environmentService,
@@ -98,9 +99,12 @@ describe("ConfigService", () => {
: serverConfigFactory(activeApiUrl + userId, tooOld);
const globalStored =
configStateDescription === "missing"
? {}
? {
[activeApiUrl]: null,
}
: {
[activeApiUrl]: serverConfigFactory(activeApiUrl, tooOld),
[activeApiUrl + "0"]: serverConfigFactory(activeApiUrl + userId, tooOld),
};
beforeEach(() => {
@@ -108,11 +112,6 @@ describe("ConfigService", () => {
userState.nextState(userStored);
});
// sanity check
test("authed and unauthorized state are different", () => {
expect(globalStored[activeApiUrl]).not.toEqual(userStored);
});
describe("fail to fetch", () => {
beforeEach(() => {
configApiService.get.mockRejectedValue(new Error("Unable to fetch"));
@@ -178,6 +177,7 @@ describe("ConfigService", () => {
beforeEach(() => {
globalState.stateSubject.next(globalStored);
userState.nextState(userStored);
Matrix.autoMockMethod(environmentService.getEnvironment$, () => environmentSubject);
});
it("does not fetch from server", async () => {
await firstValueFrom(sut.serverConfig$);
@@ -189,21 +189,13 @@ describe("ConfigService", () => {
const actual = await firstValueFrom(sut.serverConfig$);
expect(actual).toEqual(activeUserId ? userStored : globalStored[activeApiUrl]);
});
it("does not complete after emit", async () => {
const emissions = [];
const subscription = sut.serverConfig$.subscribe((v) => emissions.push(v));
await awaitAsync();
expect(emissions.length).toBe(1);
expect(subscription.closed).toBe(false);
});
});
});
});
it("gets global config when there is an locked active user", async () => {
await accountService.switchAccount(userId);
environmentService.environment$ = of(environmentFactory(activeApiUrl));
environmentService.globalEnvironment$ = of(environmentFactory(activeApiUrl));
globalState.stateSubject.next({
[activeApiUrl]: serverConfigFactory(activeApiUrl + "global"),
@@ -236,7 +228,8 @@ describe("ConfigService", () => {
beforeEach(() => {
environmentSubject = new Subject<Environment>();
environmentService.environment$ = environmentSubject;
environmentService.globalEnvironment$ = environmentSubject;
Matrix.autoMockMethod(environmentService.getEnvironment$, () => environmentSubject);
sut = new DefaultConfigService(
configApiService,
environmentService,
@@ -327,7 +320,8 @@ describe("ConfigService", () => {
beforeEach(async () => {
const config = serverConfigFactory("existing-data", tooOld);
environmentService.environment$ = environmentSubject;
environmentService.globalEnvironment$ = environmentSubject;
Matrix.autoMockMethod(environmentService.getEnvironment$, () => environmentSubject);
globalState.stateSubject.next({ [apiUrl(0)]: config });
userState.stateSubject.next({

View File

@@ -1,17 +1,18 @@
// FIXME: Update this file to be type safe and remove this and next line
// @ts-strict-ignore
import {
combineLatest,
distinctUntilChanged,
firstValueFrom,
map,
mergeWith,
NEVER,
Observable,
of,
shareReplay,
ReplaySubject,
share,
Subject,
switchMap,
tap,
timer,
} from "rxjs";
import { SemVer } from "semver";
@@ -50,11 +51,15 @@ export const GLOBAL_SERVER_CONFIGURATIONS = KeyDefinition.record<ServerConfig, A
},
);
const environmentComparer = (previous: Environment, current: Environment) => {
return previous.getApiUrl() === current.getApiUrl();
};
// 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 failedFetchFallbackSubject = new Subject<ServerConfig | null>();
serverConfig$: Observable<ServerConfig>;
serverConfig$: Observable<ServerConfig | null>;
serverSettings$: Observable<ServerSettings>;
@@ -67,32 +72,61 @@ export class DefaultConfigService implements ConfigService {
private stateProvider: StateProvider,
private authService: AuthService,
) {
const userId$ = this.stateProvider.activeUserId$;
const authStatus$ = userId$.pipe(
switchMap((userId) => (userId == null ? of(null) : this.authService.authStatusFor$(userId))),
const globalConfig$ = this.environmentService.globalEnvironment$.pipe(
distinctUntilChanged(environmentComparer),
switchMap((environment) =>
this.globalConfigFor$(environment.getApiUrl()).pipe(
map((config) => {
return [config, null as UserId | null, environment, config] as const;
}),
),
),
);
this.serverConfig$ = combineLatest([
userId$,
this.environmentService.environment$,
authStatus$,
]).pipe(
switchMap(([userId, environment, authStatus]) => {
if (userId == null || authStatus !== AuthenticationStatus.Unlocked) {
return this.globalConfigFor$(environment.getApiUrl()).pipe(
map((config) => [config, null, environment] as const),
);
this.serverConfig$ = this.stateProvider.activeUserId$.pipe(
distinctUntilChanged(),
switchMap((userId) => {
if (userId == null) {
// Global
return globalConfig$;
}
return this.userConfigFor$(userId).pipe(
map((config) => [config, userId, environment] as const),
return this.authService.authStatusFor$(userId).pipe(
map((authStatus) => authStatus === AuthenticationStatus.Unlocked),
distinctUntilChanged(),
switchMap((isUnlocked) => {
if (!isUnlocked) {
return globalConfig$;
}
return combineLatest([
this.environmentService
.getEnvironment$(userId)
.pipe(distinctUntilChanged(environmentComparer)),
this.userConfigFor$(userId),
]).pipe(
switchMap(([environment, config]) => {
if (config == null) {
// If the user doesn't have any config yet, use the global config for that url as the fallback
return this.globalConfigFor$(environment.getApiUrl()).pipe(
map(
(globalConfig) =>
[null as ServerConfig | null, userId, environment, globalConfig] as const,
),
);
}
return of([config, userId, environment, config] as const);
}),
);
}),
);
}),
tap(async (rec) => {
const [existingConfig, userId, environment] = rec;
const [existingConfig, userId, environment, fallbackConfig] = rec;
// Grab new config if older retrieval interval
if (!existingConfig || this.olderThanRetrievalInterval(existingConfig.utcDate)) {
await this.renewConfig(existingConfig, userId, environment);
await this.renewConfig(existingConfig, userId, environment, fallbackConfig);
}
}),
switchMap(([existingConfig]) => {
@@ -106,7 +140,7 @@ export class DefaultConfigService implements ConfigService {
}),
// If fetch fails, we'll emit on this subject to fallback to the existing config
mergeWith(this.failedFetchFallbackSubject),
shareReplay({ refCount: true, bufferSize: 1 }),
share({ connector: () => new ReplaySubject(1), resetOnRefCountZero: () => timer(1000) }),
);
this.cloudRegion$ = this.serverConfig$.pipe(
@@ -155,19 +189,18 @@ export class DefaultConfigService implements ConfigService {
// Updates the on-disk configuration with a newly retrieved configuration
private async renewConfig(
existingConfig: ServerConfig,
userId: UserId,
existingConfig: ServerConfig | null,
userId: UserId | null,
environment: Environment,
fallbackConfig: ServerConfig | null,
): Promise<void> {
try {
// Feature flags often have a big impact on user experience, lets ensure we return some value
// somewhat quickly even though it may not be accurate, we won't cancel the HTTP request
// though so that hopefully it can have finished and hydrated a more accurate value.
const handle = setTimeout(() => {
this.logService.info(
"Self-host environment did not respond in time, emitting previous config.",
);
this.failedFetchFallbackSubject.next(existingConfig);
this.logService.info("Environment did not respond in time, emitting previous config.");
this.failedFetchFallbackSubject.next(fallbackConfig);
}, SLOW_EMISSION_GUARD);
const response = await this.configApiService.get(userId);
clearTimeout(handle);
@@ -195,17 +228,17 @@ export class DefaultConfigService implements ConfigService {
// mutate error to be handled by catchError
this.logService.error(`Unable to fetch ServerConfig from ${environment.getApiUrl()}`, e);
// Emit the existing config
this.failedFetchFallbackSubject.next(existingConfig);
this.failedFetchFallbackSubject.next(fallbackConfig);
}
}
private globalConfigFor$(apiUrl: string): Observable<ServerConfig> {
private globalConfigFor$(apiUrl: string): Observable<ServerConfig | null> {
return this.stateProvider
.getGlobal(GLOBAL_SERVER_CONFIGURATIONS)
.state$.pipe(map((configs) => configs?.[apiUrl]));
.state$.pipe(map((configs) => configs?.[apiUrl] ?? null));
}
private userConfigFor$(userId: UserId): Observable<ServerConfig> {
private userConfigFor$(userId: UserId): Observable<ServerConfig | null> {
return this.stateProvider.getUser(userId, USER_SERVER_CONFIG).state$;
}
}

View File

@@ -133,6 +133,7 @@ export class DefaultEnvironmentService implements EnvironmentService {
);
environment$: Observable<Environment>;
globalEnvironment$: Observable<Environment>;
cloudWebVaultUrl$: Observable<string>;
constructor(
@@ -148,6 +149,10 @@ export class DefaultEnvironmentService implements EnvironmentService {
distinctUntilChanged((oldUserId: UserId, newUserId: UserId) => oldUserId == newUserId),
);
this.globalEnvironment$ = this.stateProvider
.getGlobal(GLOBAL_ENVIRONMENT_KEY)
.state$.pipe(map((state) => this.buildEnvironment(state?.region, state?.urls)));
this.environment$ = account$.pipe(
switchMap((userId) => {
const t = userId
@@ -263,7 +268,7 @@ export class DefaultEnvironmentService implements EnvironmentService {
return new SelfHostedEnvironment(urls);
}
async setCloudRegion(userId: UserId, region: CloudRegion) {
async setCloudRegion(userId: UserId | null, region: CloudRegion) {
if (userId == null) {
await this.globalCloudRegionState.update(() => region);
} else {
@@ -271,7 +276,7 @@ export class DefaultEnvironmentService implements EnvironmentService {
}
}
getEnvironment$(userId: UserId): Observable<Environment | undefined> {
getEnvironment$(userId: UserId): Observable<Environment> {
return this.stateProvider.getUser(userId, USER_ENVIRONMENT_KEY).state$.pipe(
map((state) => {
return this.buildEnvironment(state?.region, state?.urls);

View File

@@ -1,92 +1,2 @@
// FIXME: Update this file to be type safe and remove this and next line
// @ts-strict-ignore
// This import has been flagged as unallowed for this class. It may be involved in a circular dependency loop.
// eslint-disable-next-line no-restricted-imports
import { KdfConfig } from "@bitwarden/key-management";
import { PureCrypto } from "@bitwarden/sdk-internal";
import { CryptoFunctionService } from "../../key-management/crypto/abstractions/crypto-function.service";
import { CsprngArray } from "../../types/csprng";
import { KeyGenerationService as KeyGenerationServiceAbstraction } from "../abstractions/key-generation.service";
import { SdkLoadService } from "../abstractions/sdk/sdk-load.service";
import { EncryptionType } from "../enums";
import { Utils } from "../misc/utils";
import { SymmetricCryptoKey } from "../models/domain/symmetric-crypto-key";
export class KeyGenerationService implements KeyGenerationServiceAbstraction {
constructor(private cryptoFunctionService: CryptoFunctionService) {}
async createKey(bitLength: 256 | 512): Promise<SymmetricCryptoKey> {
const key = await this.cryptoFunctionService.aesGenerateKey(bitLength);
return new SymmetricCryptoKey(key);
}
async createKeyWithPurpose(
bitLength: 128 | 192 | 256 | 512,
purpose: string,
salt?: string,
): Promise<{ salt: string; material: CsprngArray; derivedKey: SymmetricCryptoKey }> {
if (salt == null) {
const bytes = await this.cryptoFunctionService.randomBytes(32);
salt = Utils.fromBufferToUtf8(bytes);
}
const material = await this.cryptoFunctionService.aesGenerateKey(bitLength);
const key = await this.cryptoFunctionService.hkdf(material, salt, purpose, 64, "sha256");
return { salt, material, derivedKey: new SymmetricCryptoKey(key) };
}
async deriveKeyFromMaterial(
material: CsprngArray,
salt: string,
purpose: string,
): Promise<SymmetricCryptoKey> {
const key = await this.cryptoFunctionService.hkdf(material, salt, purpose, 64, "sha256");
return new SymmetricCryptoKey(key);
}
async deriveKeyFromPassword(
password: string | Uint8Array,
salt: string | Uint8Array,
kdfConfig: KdfConfig,
): Promise<SymmetricCryptoKey> {
if (typeof password === "string") {
password = new TextEncoder().encode(password);
}
if (typeof salt === "string") {
salt = new TextEncoder().encode(salt);
}
await SdkLoadService.Ready;
return new SymmetricCryptoKey(
PureCrypto.derive_kdf_material(password, salt, kdfConfig.toSdkConfig()),
);
}
async stretchKey(key: SymmetricCryptoKey): Promise<SymmetricCryptoKey> {
// The key to be stretched is actually usually the output of a KDF, and not actually meant for AesCbc256_B64 encryption,
// but has the same key length. Only 256-bit key materials should be stretched.
if (key.inner().type != EncryptionType.AesCbc256_B64) {
throw new Error("Key passed into stretchKey is not a 256-bit key.");
}
const newKey = new Uint8Array(64);
// Master key and pin key are always 32 bytes
const encKey = await this.cryptoFunctionService.hkdfExpand(
key.inner().encryptionKey,
"enc",
32,
"sha256",
);
const macKey = await this.cryptoFunctionService.hkdfExpand(
key.inner().encryptionKey,
"mac",
32,
"sha256",
);
newKey.set(new Uint8Array(encKey));
newKey.set(new Uint8Array(macKey), 32);
return new SymmetricCryptoKey(newKey);
}
}
/** Temporary re-export. This should not be used for new imports */
export { DefaultKeyGenerationService as KeyGenerationService } from "../../key-management/crypto/key-generation/default-key-generation.service";

View File

@@ -197,7 +197,6 @@ export class ApiService implements ApiServiceAbstraction {
if (this.customUserAgent != null) {
headers.set("User-Agent", this.customUserAgent);
}
request.alterIdentityTokenHeaders(headers);
const identityToken =
request instanceof UserApiTokenRequest

View File

@@ -20,3 +20,8 @@ export type OrganizationIntegrationConfigurationId = Opaque<
string,
"OrganizationIntegrationConfigurationId"
>;
/**
* A string representation of an empty guid.
*/
export const emptyGuid = "00000000-0000-0000-0000-000000000000";

View File

@@ -1,5 +1,5 @@
import { mock } from "jest-mock-extended";
import { BehaviorSubject, map, of } from "rxjs";
import { BehaviorSubject, filter, firstValueFrom, map, of } from "rxjs";
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
@@ -7,6 +7,7 @@ import { CipherResponse } from "@bitwarden/common/vault/models/response/cipher.r
// This import has been flagged as unallowed for this class. It may be involved in a circular dependency loop.
// eslint-disable-next-line no-restricted-imports
import { CipherDecryptionKeys, KeyService } from "@bitwarden/key-management";
import { MessageSender } from "@bitwarden/messaging";
import { FakeAccountService, mockAccountServiceWith } from "../../../spec/fake-account-service";
import { FakeStateProvider } from "../../../spec/fake-state-provider";
@@ -106,6 +107,7 @@ describe("Cipher Service", () => {
const logService = mock<LogService>();
const stateProvider = new FakeStateProvider(accountService);
const cipherEncryptionService = mock<CipherEncryptionService>();
const messageSender = mock<MessageSender>();
const userId = "TestUserId" as UserId;
const orgId = "4ff8c0b2-1d3e-4f8c-9b2d-1d3e4f8c0b2" as OrganizationId;
@@ -134,6 +136,7 @@ describe("Cipher Service", () => {
accountService,
logService,
cipherEncryptionService,
messageSender,
);
encryptionContext = { cipher: new Cipher(cipherData), encryptedFor: userId };
@@ -551,6 +554,23 @@ describe("Cipher Service", () => {
newUserKey,
);
});
it("sends overlay update when cipherViews$ emits", async () => {
(cipherService.cipherViews$ as jest.Mock)?.mockRestore();
const decryptedView = new CipherView(encryptionContext.cipher);
jest.spyOn(cipherService, "getAllDecrypted").mockResolvedValue([decryptedView]);
const sendSpy = jest.spyOn(messageSender, "send");
await firstValueFrom(
cipherService
.cipherViews$(mockUserId)
.pipe(filter((cipherViews): cipherViews is CipherView[] => cipherViews != null)),
);
expect(sendSpy).toHaveBeenCalledWith("updateOverlayCiphers");
expect(sendSpy).toHaveBeenCalledTimes(1);
});
});
describe("decrypt", () => {

View File

@@ -1,9 +1,19 @@
// FIXME: Update this file to be type safe and remove this and next line
// @ts-strict-ignore
import { combineLatest, filter, firstValueFrom, map, Observable, Subject, switchMap } from "rxjs";
import {
combineLatest,
filter,
firstValueFrom,
map,
Observable,
Subject,
switchMap,
tap,
} from "rxjs";
import { SemVer } from "semver";
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
import { MessageSender } from "@bitwarden/common/platform/messaging";
// This import has been flagged as unallowed for this class. It may be involved in a circular dependency loop.
// eslint-disable-next-line no-restricted-imports
import { KeyService } from "@bitwarden/key-management";
@@ -109,6 +119,7 @@ export class CipherService implements CipherServiceAbstraction {
private accountService: AccountService,
private logService: LogService,
private cipherEncryptionService: CipherEncryptionService,
private messageSender: MessageSender,
) {}
localData$(userId: UserId): Observable<Record<CipherId, LocalData>> {
@@ -174,6 +185,10 @@ export class CipherService implements CipherServiceAbstraction {
]).pipe(
filter(([ciphers, _, keys]) => ciphers != null && keys != null), // Skip if ciphers haven't been loaded yor synced yet
switchMap(() => this.getAllDecrypted(userId)),
tap(async (decrypted) => {
await this.searchService.indexCiphers(userId, decrypted);
this.messageSender.send("updateOverlayCiphers");
}),
);
}, this.clearCipherViewsForUser$);
@@ -657,13 +672,14 @@ export class CipherService implements CipherServiceAbstraction {
}
async getManyFromApiForOrganization(organizationId: string): Promise<CipherView[]> {
const response = await this.apiService.send(
const r = await this.apiService.send(
"GET",
"/ciphers/organization-details/assigned?organizationId=" + organizationId,
null,
true,
true,
);
const response = new ListResponse(r, CipherResponse);
return this.decryptOrganizationCiphersResponse(response, organizationId);
}

View File

@@ -67,6 +67,13 @@ describe("RestrictedItemTypesService", () => {
expect(result).toEqual([]);
});
it("emits empty array if no account is active", async () => {
accountService.activeAccount$ = of(null);
const result = await firstValueFrom(service.restricted$);
expect(result).toEqual([]);
});
it("emits empty array if no organizations exist", async () => {
organizationService.organizations$.mockReturnValue(of([]));
policyService.policiesByType$.mockReturnValue(of([]));

View File

@@ -5,7 +5,7 @@ import { OrganizationService } from "@bitwarden/common/admin-console/abstraction
import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction";
import { PolicyType } from "@bitwarden/common/admin-console/enums";
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
import { getUserId } from "@bitwarden/common/auth/services/account.service";
import { getOptionalUserId } from "@bitwarden/common/auth/services/account.service";
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
import { CipherType } from "@bitwarden/common/vault/enums";
@@ -32,39 +32,43 @@ export class RestrictedItemTypesService {
return of([]);
}
return this.accountService.activeAccount$.pipe(
getUserId,
switchMap((userId) =>
combineLatest([
getOptionalUserId,
switchMap((userId) => {
if (userId == null) {
return of([]); // No user logged in, no restrictions
}
return combineLatest([
this.organizationService.organizations$(userId),
this.policyService.policiesByType$(PolicyType.RestrictedItemTypes, userId),
]),
),
map(([orgs, enabledPolicies]) => {
// Helper to extract restricted types, defaulting to [Card]
const restrictedTypes = (p: (typeof enabledPolicies)[number]) =>
(p.data as CipherType[]) ?? [CipherType.Card];
]).pipe(
map(([orgs, enabledPolicies]) => {
// Helper to extract restricted types, defaulting to [Card]
const restrictedTypes = (p: (typeof enabledPolicies)[number]) =>
(p.data as CipherType[]) ?? [CipherType.Card];
// Union across all enabled policies
const allRestrictedTypes = Array.from(
new Set(enabledPolicies.flatMap(restrictedTypes)),
// Union across all enabled policies
const allRestrictedTypes = Array.from(
new Set(enabledPolicies.flatMap(restrictedTypes)),
);
return allRestrictedTypes.map((cipherType) => {
// Determine which orgs allow viewing this type
const allowViewOrgIds = orgs
.filter((org) => {
const orgPolicy = enabledPolicies.find((p) => p.organizationId === org.id);
// no policy for this org => allows everything
if (!orgPolicy) {
return true;
}
// if this type not in their restricted list => they allow it
return !restrictedTypes(orgPolicy).includes(cipherType);
})
.map((org) => org.id);
return { cipherType, allowViewOrgIds };
});
}),
);
return allRestrictedTypes.map((cipherType) => {
// Determine which orgs allow viewing this type
const allowViewOrgIds = orgs
.filter((org) => {
const orgPolicy = enabledPolicies.find((p) => p.organizationId === org.id);
// no policy for this org => allows everything
if (!orgPolicy) {
return true;
}
// if this type not in their restricted list => they allow it
return !restrictedTypes(orgPolicy).includes(cipherType);
})
.map((org) => org.id);
return { cipherType, allowViewOrgIds };
});
}),
);
}),