mirror of
https://github.com/bitwarden/browser
synced 2026-02-18 18:33:50 +00:00
Added batch encrypt many method and used that in imports (#18266)
This commit is contained in:
committed by
jaasen-livefront
parent
b0162b4c91
commit
bc1ba01b40
@@ -20,6 +20,16 @@ export abstract class CipherEncryptionService {
|
||||
*/
|
||||
abstract encrypt(model: CipherView, userId: UserId): Promise<EncryptionContext | undefined>;
|
||||
|
||||
/**
|
||||
* Encrypts multiple ciphers using the SDK for the given userId.
|
||||
*
|
||||
* @param models The cipher views to encrypt
|
||||
* @param userId The user ID to initialize the SDK client with
|
||||
*
|
||||
* @returns A promise that resolves to an array of encryption contexts
|
||||
*/
|
||||
abstract encryptMany(models: CipherView[], userId: UserId): Promise<EncryptionContext[]>;
|
||||
|
||||
/**
|
||||
* Move the cipher to the specified organization by re-encrypting its keys with the organization's key.
|
||||
* The cipher.organizationId will be updated to the new organizationId.
|
||||
|
||||
@@ -50,6 +50,15 @@ export abstract class CipherService implements UserKeyRotationDataProvider<Ciphe
|
||||
keyForCipherKeyDecryption?: SymmetricCryptoKey,
|
||||
originalCipher?: Cipher,
|
||||
): Promise<EncryptionContext>;
|
||||
/**
|
||||
* Encrypts multiple ciphers for the given user.
|
||||
*
|
||||
* @param models The cipher views to encrypt
|
||||
* @param userId The user ID to encrypt for
|
||||
*
|
||||
* @returns A promise that resolves to an array of encryption contexts
|
||||
*/
|
||||
abstract encryptMany(models: CipherView[], userId: UserId): Promise<EncryptionContext[]>;
|
||||
abstract encryptFields(fieldsModel: FieldView[], key: SymmetricCryptoKey): Promise<Field[]>;
|
||||
abstract encryptField(fieldModel: FieldView, key: SymmetricCryptoKey): Promise<Field>;
|
||||
abstract get(id: string, userId: UserId): Promise<Cipher>;
|
||||
|
||||
@@ -340,6 +340,24 @@ export class CipherService implements CipherServiceAbstraction {
|
||||
}
|
||||
}
|
||||
|
||||
async encryptMany(models: CipherView[], userId: UserId): Promise<EncryptionContext[]> {
|
||||
const sdkEncryptionEnabled = await this.configService.getFeatureFlag(
|
||||
FeatureFlag.PM22136_SdkCipherEncryption,
|
||||
);
|
||||
|
||||
if (sdkEncryptionEnabled) {
|
||||
return await this.cipherEncryptionService.encryptMany(models, userId);
|
||||
}
|
||||
|
||||
// Fallback to sequential encryption if SDK disabled
|
||||
const results: EncryptionContext[] = [];
|
||||
for (const model of models) {
|
||||
const result = await this.encrypt(model, userId);
|
||||
results.push(result);
|
||||
}
|
||||
return results;
|
||||
}
|
||||
|
||||
async encryptAttachments(
|
||||
attachmentsModel: AttachmentView[],
|
||||
key: SymmetricCryptoKey,
|
||||
|
||||
@@ -253,6 +253,68 @@ describe("DefaultCipherEncryptionService", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("encryptMany", () => {
|
||||
it("should encrypt multiple ciphers", async () => {
|
||||
const cipherView2 = new CipherView(cipherObj);
|
||||
cipherView2.name = "test-name-2";
|
||||
const cipherView3 = new CipherView(cipherObj);
|
||||
cipherView3.name = "test-name-3";
|
||||
|
||||
const ciphers = [cipherViewObj, cipherView2, cipherView3];
|
||||
|
||||
const expectedCipher1: Cipher = {
|
||||
id: cipherId as string,
|
||||
type: CipherType.Login,
|
||||
name: "encrypted-name-1",
|
||||
} as unknown as Cipher;
|
||||
|
||||
const expectedCipher2: Cipher = {
|
||||
id: cipherId as string,
|
||||
type: CipherType.Login,
|
||||
name: "encrypted-name-2",
|
||||
} as unknown as Cipher;
|
||||
|
||||
const expectedCipher3: Cipher = {
|
||||
id: cipherId as string,
|
||||
type: CipherType.Login,
|
||||
name: "encrypted-name-3",
|
||||
} as unknown as Cipher;
|
||||
|
||||
mockSdkClient.vault().ciphers().encrypt.mockReturnValue({
|
||||
cipher: sdkCipher,
|
||||
encryptedFor: userId,
|
||||
});
|
||||
|
||||
jest
|
||||
.spyOn(Cipher, "fromSdkCipher")
|
||||
.mockReturnValueOnce(expectedCipher1)
|
||||
.mockReturnValueOnce(expectedCipher2)
|
||||
.mockReturnValueOnce(expectedCipher3);
|
||||
|
||||
const results = await cipherEncryptionService.encryptMany(ciphers, userId);
|
||||
|
||||
expect(results).toBeDefined();
|
||||
expect(results.length).toBe(3);
|
||||
expect(results[0].cipher).toEqual(expectedCipher1);
|
||||
expect(results[1].cipher).toEqual(expectedCipher2);
|
||||
expect(results[2].cipher).toEqual(expectedCipher3);
|
||||
|
||||
expect(mockSdkClient.vault().ciphers().encrypt).toHaveBeenCalledTimes(3);
|
||||
|
||||
expect(results[0].encryptedFor).toBe(userId);
|
||||
expect(results[1].encryptedFor).toBe(userId);
|
||||
expect(results[2].encryptedFor).toBe(userId);
|
||||
});
|
||||
|
||||
it("should handle empty array", async () => {
|
||||
const results = await cipherEncryptionService.encryptMany([], userId);
|
||||
|
||||
expect(results).toBeDefined();
|
||||
expect(results.length).toBe(0);
|
||||
expect(mockSdkClient.vault().ciphers().encrypt).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe("encryptCipherForRotation", () => {
|
||||
it("should call the sdk method to encrypt the cipher with a new key for rotation", async () => {
|
||||
mockSdkClient.vault().ciphers().encrypt_cipher_for_rotation.mockReturnValue({
|
||||
|
||||
@@ -51,6 +51,44 @@ export class DefaultCipherEncryptionService implements CipherEncryptionService {
|
||||
);
|
||||
}
|
||||
|
||||
async encryptMany(models: CipherView[], userId: UserId): Promise<EncryptionContext[]> {
|
||||
if (!models || models.length === 0) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return firstValueFrom(
|
||||
this.sdkService.userClient$(userId).pipe(
|
||||
map((sdk) => {
|
||||
if (!sdk) {
|
||||
throw new Error("SDK not available");
|
||||
}
|
||||
|
||||
using ref = sdk.take();
|
||||
|
||||
const results: EncryptionContext[] = [];
|
||||
|
||||
// TODO: https://bitwarden.atlassian.net/browse/PM-30580
|
||||
// Replace this loop with a native SDK encryptMany method for better performance.
|
||||
for (const model of models) {
|
||||
const sdkCipherView = this.toSdkCipherView(model, ref.value);
|
||||
const encryptionContext = ref.value.vault().ciphers().encrypt(sdkCipherView);
|
||||
|
||||
results.push({
|
||||
cipher: Cipher.fromSdkCipher(encryptionContext.cipher)!,
|
||||
encryptedFor: uuidAsString(encryptionContext.encryptedFor) as UserId,
|
||||
});
|
||||
}
|
||||
|
||||
return results;
|
||||
}),
|
||||
catchError((error: unknown) => {
|
||||
this.logService.error(`Failed to encrypt ciphers in batch: ${error}`);
|
||||
return EMPTY;
|
||||
}),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
async moveToOrganization(
|
||||
model: CipherView,
|
||||
organizationId: OrganizationId,
|
||||
|
||||
@@ -374,10 +374,13 @@ export class ImportService implements ImportServiceAbstraction {
|
||||
|
||||
private async handleIndividualImport(importResult: ImportResult, userId: UserId) {
|
||||
const request = new ImportCiphersRequest();
|
||||
for (let i = 0; i < importResult.ciphers.length; i++) {
|
||||
const c = await this.cipherService.encrypt(importResult.ciphers[i], userId);
|
||||
request.ciphers.push(new CipherRequest(c));
|
||||
|
||||
const encryptedCiphers = await this.cipherService.encryptMany(importResult.ciphers, userId);
|
||||
|
||||
for (const encryptedCipher of encryptedCiphers) {
|
||||
request.ciphers.push(new CipherRequest(encryptedCipher));
|
||||
}
|
||||
|
||||
const userKey = await firstValueFrom(this.keyService.userKey$(userId));
|
||||
|
||||
if (importResult.folders != null) {
|
||||
@@ -400,11 +403,18 @@ export class ImportService implements ImportServiceAbstraction {
|
||||
userId: UserId,
|
||||
) {
|
||||
const request = new ImportOrganizationCiphersRequest();
|
||||
for (let i = 0; i < importResult.ciphers.length; i++) {
|
||||
importResult.ciphers[i].organizationId = organizationId;
|
||||
const c = await this.cipherService.encrypt(importResult.ciphers[i], userId);
|
||||
request.ciphers.push(new CipherRequest(c));
|
||||
|
||||
// Set organization ID on all ciphers before batch encryption
|
||||
importResult.ciphers.forEach((cipher) => {
|
||||
cipher.organizationId = organizationId;
|
||||
});
|
||||
|
||||
const encryptedCiphers = await this.cipherService.encryptMany(importResult.ciphers, userId);
|
||||
|
||||
for (const encryptedCipher of encryptedCiphers) {
|
||||
request.ciphers.push(new CipherRequest(encryptedCipher));
|
||||
}
|
||||
|
||||
if (importResult.collections != null) {
|
||||
for (let i = 0; i < importResult.collections.length; i++) {
|
||||
importResult.collections[i].organizationId = organizationId;
|
||||
|
||||
Reference in New Issue
Block a user