1
0
mirror of https://github.com/bitwarden/browser synced 2026-02-12 06:23:38 +00:00

Merge remote-tracking branch 'origin/main' into playwright

This commit is contained in:
Matt Gibson
2026-01-26 12:57:05 -08:00
1790 changed files with 150488 additions and 32025 deletions

View File

@@ -1,8 +1,8 @@
// FIXME: Update this file to be type safe and remove this and next line
// @ts-strict-ignore
import { KeyGenerationService } from "@bitwarden/common/key-management/crypto";
import { CryptoFunctionService } from "@bitwarden/common/key-management/crypto/abstractions/crypto-function.service";
import { EncryptService } from "@bitwarden/common/key-management/crypto/abstractions/encrypt.service";
import { PinServiceAbstraction } from "@bitwarden/common/key-management/pin/pin.service.abstraction";
import { Utils } from "@bitwarden/common/platform/misc/utils";
import { UserId } from "@bitwarden/common/types/guid";
import { CipherType } from "@bitwarden/common/vault/enums";
@@ -12,7 +12,7 @@ import { KdfConfig, KdfConfigService, KdfType } from "@bitwarden/key-management"
import { BitwardenCsvExportType, BitwardenPasswordProtectedFileFormat } from "../types";
export class BaseVaultExportService {
constructor(
protected pinService: PinServiceAbstraction,
protected keyGenerationService: KeyGenerationService,
protected encryptService: EncryptService,
private cryptoFunctionService: CryptoFunctionService,
private kdfConfigService: KdfConfigService,
@@ -26,7 +26,8 @@ export class BaseVaultExportService {
const kdfConfig: KdfConfig = await this.kdfConfigService.getKdfConfig(userId);
const salt = Utils.fromBufferToB64(await this.cryptoFunctionService.randomBytes(16));
const key = await this.pinService.makePinKey(password, salt, kdfConfig);
const key = await this.keyGenerationService.deriveVaultExportKey(password, salt, kdfConfig);
const encKeyValidation = await this.encryptService.encryptString(Utils.newGuid(), key);
const encText = await this.encryptService.encryptString(clearText, key);

View File

@@ -3,13 +3,13 @@ import * as JSZip from "jszip";
import { BehaviorSubject, of } from "rxjs";
import { ApiService } from "@bitwarden/common/abstractions/api.service";
import { KeyGenerationService } from "@bitwarden/common/key-management/crypto";
import { CryptoFunctionService } from "@bitwarden/common/key-management/crypto/abstractions/crypto-function.service";
import { EncryptService } from "@bitwarden/common/key-management/crypto/abstractions/encrypt.service";
import {
EncryptedString,
EncString,
} from "@bitwarden/common/key-management/crypto/models/enc-string";
import { PinServiceAbstraction } from "@bitwarden/common/key-management/pin/pin.service.abstraction";
import { CipherWithIdExport } from "@bitwarden/common/models/export/cipher-with-ids.export";
import { Utils } from "@bitwarden/common/platform/misc/utils";
import { CipherId, emptyGuid, UserId } from "@bitwarden/common/types/guid";
@@ -169,7 +169,7 @@ describe("VaultExportService", () => {
let exportService: IndividualVaultExportService;
let cryptoFunctionService: MockProxy<CryptoFunctionService>;
let cipherService: MockProxy<CipherService>;
let pinService: MockProxy<PinServiceAbstraction>;
let keyGenerationService: MockProxy<KeyGenerationService>;
let folderService: MockProxy<FolderService>;
let keyService: MockProxy<KeyService>;
let encryptService: MockProxy<EncryptService>;
@@ -184,7 +184,7 @@ describe("VaultExportService", () => {
beforeEach(() => {
cryptoFunctionService = mock<CryptoFunctionService>();
cipherService = mock<CipherService>();
pinService = mock<PinServiceAbstraction>();
keyGenerationService = mock<KeyGenerationService>();
folderService = mock<FolderService>();
keyService = mock<KeyService>();
encryptService = mock<EncryptService>();
@@ -220,7 +220,7 @@ describe("VaultExportService", () => {
exportService = new IndividualVaultExportService(
folderService,
cipherService,
pinService,
keyGenerationService,
keyService,
encryptService,
cryptoFunctionService,
@@ -525,6 +525,20 @@ describe("VaultExportService", () => {
const exportedData = actual as ExportedVaultAsString;
expectEqualFolders(UserFolders, exportedData.data);
});
it("does not export the key property in unencrypted exports", async () => {
// Create a cipher with a key property
const cipherWithKey = generateCipherView(false);
(cipherWithKey as any).key = "shouldBeDeleted";
cipherService.getAllDecrypted.mockResolvedValue([cipherWithKey]);
const actual = await exportService.getExport(userId, "json");
expect(typeof actual.data).toBe("string");
const exportedData = actual as ExportedVaultAsString;
const parsed = JSON.parse(exportedData.data);
expect(parsed.items.length).toBe(1);
expect(parsed.items[0].key).toBeUndefined();
});
});
export class FolderResponse {

View File

@@ -5,9 +5,9 @@ import * as papa from "papaparse";
import { firstValueFrom } from "rxjs";
import { ApiService } from "@bitwarden/common/abstractions/api.service";
import { KeyGenerationService } from "@bitwarden/common/key-management/crypto";
import { CryptoFunctionService } from "@bitwarden/common/key-management/crypto/abstractions/crypto-function.service";
import { EncryptService } from "@bitwarden/common/key-management/crypto/abstractions/encrypt.service";
import { PinServiceAbstraction } from "@bitwarden/common/key-management/pin/pin.service.abstraction";
import { CipherWithIdExport, FolderWithIdExport } from "@bitwarden/common/models/export";
import { Utils } from "@bitwarden/common/platform/misc/utils";
import { CipherId, UserId } from "@bitwarden/common/types/guid";
@@ -42,7 +42,7 @@ export class IndividualVaultExportService
constructor(
private folderService: FolderService,
private cipherService: CipherService,
pinService: PinServiceAbstraction,
keyGenerationService: KeyGenerationService,
private keyService: KeyService,
encryptService: EncryptService,
cryptoFunctionService: CryptoFunctionService,
@@ -50,7 +50,7 @@ export class IndividualVaultExportService
private apiService: ApiService,
private restrictedItemTypesService: RestrictedItemTypesService,
) {
super(pinService, encryptService, cryptoFunctionService, kdfConfigService);
super(keyGenerationService, encryptService, cryptoFunctionService, kdfConfigService);
}
/** Creates an export of an individual vault (My Vault). Based on the provided format it will either be unencrypted, encrypted or password protected and in case zip is selected will include attachments
@@ -317,6 +317,7 @@ export class IndividualVaultExportService
const cipher = new CipherWithIdExport();
cipher.build(c);
cipher.collectionIds = null;
delete cipher.key;
jsonDoc.items.push(cipher);
});

View File

@@ -3,16 +3,16 @@
import * as papa from "papaparse";
import { filter, firstValueFrom, map } from "rxjs";
import { CollectionService } from "@bitwarden/admin-console/common";
import {
CollectionService,
CollectionData,
Collection,
CollectionDetailsResponse,
CollectionView,
} from "@bitwarden/admin-console/common";
CollectionDetailsResponse,
Collection,
CollectionData,
} from "@bitwarden/common/admin-console/models/collections";
import { KeyGenerationService } from "@bitwarden/common/key-management/crypto";
import { CryptoFunctionService } from "@bitwarden/common/key-management/crypto/abstractions/crypto-function.service";
import { EncryptService } from "@bitwarden/common/key-management/crypto/abstractions/encrypt.service";
import { PinServiceAbstraction } from "@bitwarden/common/key-management/pin/pin.service.abstraction";
import { CipherWithIdExport, CollectionWithIdExport } from "@bitwarden/common/models/export";
import { Utils } from "@bitwarden/common/platform/misc/utils";
import { SymmetricCryptoKey } from "@bitwarden/common/platform/models/domain/symmetric-crypto-key";
@@ -46,7 +46,7 @@ export class OrganizationVaultExportService
constructor(
private cipherService: CipherService,
private vaultExportApiService: VaultExportApiService,
pinService: PinServiceAbstraction,
keyGenerationService: KeyGenerationService,
private keyService: KeyService,
encryptService: EncryptService,
cryptoFunctionService: CryptoFunctionService,
@@ -54,7 +54,7 @@ export class OrganizationVaultExportService
kdfConfigService: KdfConfigService,
private restrictedItemTypesService: RestrictedItemTypesService,
) {
super(pinService, encryptService, cryptoFunctionService, kdfConfigService);
super(keyGenerationService, encryptService, cryptoFunctionService, kdfConfigService);
}
/** Creates a password protected export of an organizational vault.
@@ -383,6 +383,7 @@ export class OrganizationVaultExportService
decCiphers.forEach((c) => {
const cipher = new CipherWithIdExport();
cipher.build(c);
delete cipher.key;
jsonDoc.items.push(cipher);
});
return JSON.stringify(jsonDoc, null, " ");

View File

@@ -5,42 +5,48 @@ import {
} from "@bitwarden/common/models/export";
// Base
export type BitwardenJsonExport = {
encrypted: boolean;
items: CipherWithIdExport[];
};
export type BitwardenJsonExport = BitwardenUnEncryptedJsonExport | BitwardenEncryptedJsonExport;
// Decrypted
export type BitwardenUnEncryptedJsonExport = BitwardenJsonExport & {
encrypted: false;
};
export type BitwardenUnEncryptedJsonExport =
| BitwardenUnEncryptedIndividualJsonExport
| BitwardenUnEncryptedOrgJsonExport;
export type BitwardenUnEncryptedIndividualJsonExport = BitwardenUnEncryptedJsonExport & {
export type BitwardenUnEncryptedIndividualJsonExport = {
encrypted: false;
items: CipherWithIdExport[];
folders: FolderWithIdExport[];
};
export type BitwardenUnEncryptedOrgJsonExport = BitwardenUnEncryptedJsonExport & {
export type BitwardenUnEncryptedOrgJsonExport = {
encrypted: false;
items: CipherWithIdExport[];
collections: CollectionWithIdExport[];
};
// Account-encrypted
export type BitwardenEncryptedJsonExport = BitwardenJsonExport & {
export type BitwardenEncryptedJsonExport =
| BitwardenEncryptedIndividualJsonExport
| BitwardenEncryptedOrgJsonExport;
export type BitwardenEncryptedIndividualJsonExport = {
encrypted: true;
encKeyValidation_DO_NOT_EDIT: string;
};
export type BitwardenEncryptedIndividualJsonExport = BitwardenEncryptedJsonExport & {
items: CipherWithIdExport[];
folders: FolderWithIdExport[];
};
export type BitwardenEncryptedOrgJsonExport = BitwardenEncryptedJsonExport & {
export type BitwardenEncryptedOrgJsonExport = {
encrypted: true;
encKeyValidation_DO_NOT_EDIT: string;
items: CipherWithIdExport[];
collections: CollectionWithIdExport[];
};
// Password-protected
export type BitwardenPasswordProtectedFileFormat = {
encrypted: boolean;
passwordProtected: boolean;
encrypted: true;
passwordProtected: true;
salt: string;
kdfIterations: number;
kdfMemory?: number;
@@ -49,3 +55,50 @@ export type BitwardenPasswordProtectedFileFormat = {
encKeyValidation_DO_NOT_EDIT: string;
data: string;
};
// Unencrypted type guards
export function isUnencrypted(
data: BitwardenJsonExport | null | undefined,
): data is BitwardenUnEncryptedJsonExport {
return data != null && (data as { encrypted?: unknown }).encrypted !== true;
}
export function isIndividualUnEncrypted(
data: BitwardenJsonExport | null | undefined,
): data is BitwardenUnEncryptedIndividualJsonExport {
return isUnencrypted(data) && (data as { folders?: unknown }).folders != null;
}
export function isOrgUnEncrypted(
data: BitwardenJsonExport | null | undefined,
): data is BitwardenUnEncryptedOrgJsonExport {
return isUnencrypted(data) && (data as { collections?: unknown }).collections != null;
}
// Encrypted type guards
export function isEncrypted(
data: BitwardenJsonExport | null | undefined,
): data is BitwardenEncryptedJsonExport {
return data != null && (data as { encrypted?: unknown }).encrypted === true;
}
export function isPasswordProtected(
data: BitwardenPasswordProtectedFileFormat | BitwardenJsonExport | null | undefined,
): data is BitwardenPasswordProtectedFileFormat {
return (
data != null &&
(data as { encrypted?: unknown }).encrypted === true &&
(data as { passwordProtected?: unknown }).passwordProtected === true
);
}
export function isIndividualEncrypted(
data: BitwardenJsonExport | null | undefined,
): data is BitwardenEncryptedIndividualJsonExport {
return isEncrypted(data) && (data as { folders?: unknown }).folders != null;
}
export function isOrgEncrypted(
data: BitwardenJsonExport | null | undefined,
): data is BitwardenEncryptedOrgJsonExport {
return isEncrypted(data) && (data as { collections?: unknown }).collections != null;
}

View File

@@ -18,7 +18,6 @@ import {
BehaviorSubject,
combineLatest,
firstValueFrom,
from,
map,
merge,
Observable,
@@ -43,8 +42,6 @@ import { Organization } from "@bitwarden/common/admin-console/models/domain/orga
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
import { getUserId } from "@bitwarden/common/auth/services/account.service";
import { ClientType, EventType } from "@bitwarden/common/enums";
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
import { FileDownloadService } from "@bitwarden/common/platform/abstractions/file-download/file-download.service";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
@@ -100,7 +97,6 @@ import { ExportScopeCalloutComponent } from "./export-scope-callout.component";
})
export class ExportComponent implements OnInit, OnDestroy, AfterViewInit {
private _organizationId$ = new BehaviorSubject<OrganizationId | undefined>(undefined);
private createDefaultLocationFlagEnabled$: Observable<boolean>;
private _showExcludeMyItems = false;
/**
@@ -259,13 +255,11 @@ export class ExportComponent implements OnInit, OnDestroy, AfterViewInit {
protected organizationService: OrganizationService,
private accountService: AccountService,
private collectionService: CollectionService,
private configService: ConfigService,
private platformUtilsService: PlatformUtilsService,
@Optional() private router?: Router,
) {}
async ngOnInit() {
this.observeFeatureFlags();
this.observeFormState();
this.observePolicyStatus();
this.observeFormSelections();
@@ -286,12 +280,6 @@ export class ExportComponent implements OnInit, OnDestroy, AfterViewInit {
this.setupPolicyBasedFormState();
}
private observeFeatureFlags(): void {
this.createDefaultLocationFlagEnabled$ = from(
this.configService.getFeatureFlag(FeatureFlag.CreateDefaultLocation),
).pipe(shareReplay({ bufferSize: 1, refCount: true }));
}
private observeFormState(): void {
this.exportForm.statusChanges.pipe(takeUntil(this.destroy$)).subscribe((c) => {
this.formDisabled.emit(c === "DISABLED");
@@ -380,32 +368,24 @@ export class ExportComponent implements OnInit, OnDestroy, AfterViewInit {
/**
* Determine value of showExcludeMyItems. Returns true when:
* CreateDefaultLocation feature flag is on
* AND organizationDataOwnershipPolicy is enabled for the selected organization
* organizationDataOwnershipPolicy is enabled for the selected organization
* AND a valid OrganizationId is present (not exporting from individual vault)
*/
private observeMyItemsExclusionCriteria(): void {
combineLatest({
createDefaultLocationFlagEnabled: this.createDefaultLocationFlagEnabled$,
organizationDataOwnershipPolicyEnabledForOrg:
this.organizationDataOwnershipPolicyEnabledForOrg$,
organizationId: this._organizationId$,
})
.pipe(takeUntil(this.destroy$))
.subscribe(
({
createDefaultLocationFlagEnabled,
organizationDataOwnershipPolicyEnabledForOrg,
organizationId,
}) => {
if (!createDefaultLocationFlagEnabled || !organizationId) {
this._showExcludeMyItems = false;
return;
}
.subscribe(({ organizationDataOwnershipPolicyEnabledForOrg, organizationId }) => {
if (!organizationId) {
this._showExcludeMyItems = false;
return;
}
this._showExcludeMyItems = organizationDataOwnershipPolicyEnabledForOrg;
},
);
this._showExcludeMyItems = organizationDataOwnershipPolicyEnabledForOrg;
});
}
// Setup validator adjustments based on format and encryption type changes
@@ -620,7 +600,7 @@ export class ExportComponent implements OnInit, OnDestroy, AfterViewInit {
title: "confirmVaultExport",
bodyText: confirmDescription,
confirmButtonOptions: {
text: "exportVault",
text: "continue",
type: "primary",
},
});