mirror of
https://github.com/bitwarden/browser
synced 2026-02-09 13:10:17 +00:00
decrypt attachment content using sdk
This commit is contained in:
@@ -5,11 +5,13 @@ import { ApiService } from "@bitwarden/common/abstractions/api.service";
|
||||
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||
import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions/account/billing-account-profile-state.service";
|
||||
import { EncryptService } from "@bitwarden/common/key-management/crypto/abstractions/encrypt.service";
|
||||
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";
|
||||
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
||||
import { StateService } from "@bitwarden/common/platform/abstractions/state.service";
|
||||
import { CipherEncryptionService } from "@bitwarden/common/vault/abstractions/cipher-encryption.service";
|
||||
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
|
||||
import { DialogService, ToastService } from "@bitwarden/components";
|
||||
import { KeyService } from "@bitwarden/key-management";
|
||||
@@ -33,6 +35,8 @@ export class AttachmentsComponent extends BaseAttachmentsComponent {
|
||||
billingAccountProfileStateService: BillingAccountProfileStateService,
|
||||
accountService: AccountService,
|
||||
toastService: ToastService,
|
||||
configService: ConfigService,
|
||||
cipherEncryptionService: CipherEncryptionService,
|
||||
) {
|
||||
super(
|
||||
cipherService,
|
||||
@@ -49,6 +53,8 @@ export class AttachmentsComponent extends BaseAttachmentsComponent {
|
||||
billingAccountProfileStateService,
|
||||
accountService,
|
||||
toastService,
|
||||
configService,
|
||||
cipherEncryptionService,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,21 +1,25 @@
|
||||
// FIXME: Update this file to be type safe and remove this and next line
|
||||
// @ts-strict-ignore
|
||||
import { Directive, EventEmitter, Input, OnInit, Output } from "@angular/core";
|
||||
import { firstValueFrom } from "rxjs";
|
||||
import { firstValueFrom, map } from "rxjs";
|
||||
|
||||
import { ApiService } from "@bitwarden/common/abstractions/api.service";
|
||||
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||
import { getUserId } from "@bitwarden/common/auth/services/account.service";
|
||||
import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions/account/billing-account-profile-state.service";
|
||||
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
|
||||
import { EncryptService } from "@bitwarden/common/key-management/crypto/abstractions/encrypt.service";
|
||||
import { ErrorResponse } from "@bitwarden/common/models/response/error.response";
|
||||
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";
|
||||
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
||||
import { StateService } from "@bitwarden/common/platform/abstractions/state.service";
|
||||
import { EncArrayBuffer } from "@bitwarden/common/platform/models/domain/enc-array-buffer";
|
||||
import { UserId } from "@bitwarden/common/types/guid";
|
||||
import { OrganizationId, UserId } from "@bitwarden/common/types/guid";
|
||||
import { OrgKey } from "@bitwarden/common/types/key";
|
||||
import { CipherEncryptionService } from "@bitwarden/common/vault/abstractions/cipher-encryption.service";
|
||||
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
|
||||
import { CipherData } from "@bitwarden/common/vault/models/data/cipher.data";
|
||||
import { Cipher } from "@bitwarden/common/vault/models/domain/cipher";
|
||||
@@ -56,6 +60,8 @@ export class AttachmentsComponent implements OnInit {
|
||||
protected billingAccountProfileStateService: BillingAccountProfileStateService,
|
||||
protected accountService: AccountService,
|
||||
protected toastService: ToastService,
|
||||
protected configService: ConfigService,
|
||||
protected cipherEncryptionService: CipherEncryptionService,
|
||||
) {}
|
||||
|
||||
async ngOnInit() {
|
||||
@@ -193,12 +199,9 @@ export class AttachmentsComponent implements OnInit {
|
||||
}
|
||||
|
||||
try {
|
||||
const encBuf = await EncArrayBuffer.fromResponse(response);
|
||||
const key =
|
||||
attachment.key != null
|
||||
? attachment.key
|
||||
: await this.keyService.getOrgKey(this.cipher.organizationId);
|
||||
const decBuf = await this.encryptService.decryptToBytes(encBuf, key);
|
||||
const activeUserId = await firstValueFrom(this.accountService.activeAccount$.pipe(getUserId));
|
||||
const decBuf = await this.getDecryptedBuffer(response, attachment, activeUserId);
|
||||
|
||||
this.fileDownloadService.download({
|
||||
fileName: attachment.fileName,
|
||||
blobData: decBuf,
|
||||
@@ -270,15 +273,11 @@ export class AttachmentsComponent implements OnInit {
|
||||
|
||||
try {
|
||||
// 2. Resave
|
||||
const encBuf = await EncArrayBuffer.fromResponse(response);
|
||||
const key =
|
||||
attachment.key != null
|
||||
? attachment.key
|
||||
: await this.keyService.getOrgKey(this.cipher.organizationId);
|
||||
const decBuf = await this.encryptService.decryptToBytes(encBuf, key);
|
||||
const activeUserId = await firstValueFrom(
|
||||
this.accountService.activeAccount$.pipe(getUserId),
|
||||
);
|
||||
const decBuf = await this.getDecryptedBuffer(response, attachment, activeUserId);
|
||||
|
||||
this.cipherDomain = await this.cipherService.saveAttachmentRawWithServer(
|
||||
this.cipherDomain,
|
||||
attachment.fileName,
|
||||
@@ -341,4 +340,39 @@ export class AttachmentsComponent implements OnInit {
|
||||
protected async reupload(attachment: AttachmentView) {
|
||||
// TODO: This should be removed but is needed since we re-use the same template
|
||||
}
|
||||
|
||||
private async getDecryptedBuffer(
|
||||
response: Response,
|
||||
attachment: AttachmentView,
|
||||
userId: UserId,
|
||||
): Promise<Uint8Array> {
|
||||
const useSdkDecryption = await this.configService.getFeatureFlag(
|
||||
FeatureFlag.PM19941MigrateCipherDomainToSdk,
|
||||
);
|
||||
|
||||
if (useSdkDecryption) {
|
||||
const attachmentDomain = this.cipherDomain.attachments?.find((a) => a.id === attachment.id);
|
||||
|
||||
const encArrayBuf = new Uint8Array(await response.arrayBuffer());
|
||||
return await this.cipherEncryptionService.decryptAttachmentContent(
|
||||
this.cipherDomain,
|
||||
attachmentDomain,
|
||||
encArrayBuf,
|
||||
userId,
|
||||
);
|
||||
}
|
||||
|
||||
const encBuf = await EncArrayBuffer.fromResponse(response);
|
||||
const key =
|
||||
attachment.key != null
|
||||
? attachment.key
|
||||
: await firstValueFrom(
|
||||
this.keyService
|
||||
.orgKeys$(userId)
|
||||
.pipe(
|
||||
map((orgKeys) => orgKeys[this.cipher.organizationId as OrganizationId] as OrgKey),
|
||||
),
|
||||
);
|
||||
return await this.encryptService.decryptToBytes(encBuf, key);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -31,22 +31,27 @@ import { TokenService } from "@bitwarden/common/auth/abstractions/token.service"
|
||||
import { getUserId } from "@bitwarden/common/auth/services/account.service";
|
||||
import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions/account/billing-account-profile-state.service";
|
||||
import { EventType } from "@bitwarden/common/enums";
|
||||
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
|
||||
import { EncryptService } from "@bitwarden/common/key-management/crypto/abstractions/encrypt.service";
|
||||
import { ErrorResponse } from "@bitwarden/common/models/response/error.response";
|
||||
import { BroadcasterService } from "@bitwarden/common/platform/abstractions/broadcaster.service";
|
||||
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";
|
||||
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
||||
import { StateService } from "@bitwarden/common/platform/abstractions/state.service";
|
||||
import { EncArrayBuffer } from "@bitwarden/common/platform/models/domain/enc-array-buffer";
|
||||
import { CollectionId, UserId } from "@bitwarden/common/types/guid";
|
||||
import { CipherId, CollectionId, OrganizationId, UserId } from "@bitwarden/common/types/guid";
|
||||
import { OrgKey } from "@bitwarden/common/types/key";
|
||||
import { CipherEncryptionService } from "@bitwarden/common/vault/abstractions/cipher-encryption.service";
|
||||
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
|
||||
import { FolderService } from "@bitwarden/common/vault/abstractions/folder/folder.service.abstraction";
|
||||
import { TotpService } from "@bitwarden/common/vault/abstractions/totp.service";
|
||||
import { CipherType, FieldType } from "@bitwarden/common/vault/enums";
|
||||
import { CipherRepromptType } from "@bitwarden/common/vault/enums/cipher-reprompt-type";
|
||||
import { Launchable } from "@bitwarden/common/vault/interfaces/launchable";
|
||||
import { Cipher } from "@bitwarden/common/vault/models/domain/cipher";
|
||||
import { AttachmentView } from "@bitwarden/common/vault/models/view/attachment.view";
|
||||
import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
|
||||
import { FolderView } from "@bitwarden/common/vault/models/view/folder.view";
|
||||
@@ -137,6 +142,8 @@ export class ViewComponent implements OnDestroy, OnInit {
|
||||
private billingAccountProfileStateService: BillingAccountProfileStateService,
|
||||
protected toastService: ToastService,
|
||||
private cipherAuthorizationService: CipherAuthorizationService,
|
||||
private configService: ConfigService,
|
||||
private cipherEncryptionService: CipherEncryptionService,
|
||||
) {}
|
||||
|
||||
ngOnInit() {
|
||||
@@ -458,12 +465,8 @@ export class ViewComponent implements OnDestroy, OnInit {
|
||||
}
|
||||
|
||||
try {
|
||||
const encBuf = await EncArrayBuffer.fromResponse(response);
|
||||
const key =
|
||||
attachment.key != null
|
||||
? attachment.key
|
||||
: await this.keyService.getOrgKey(this.cipher.organizationId);
|
||||
const decBuf = await this.encryptService.decryptToBytes(encBuf, key);
|
||||
const activeUserId = await firstValueFrom(this.accountService.activeAccount$.pipe(getUserId));
|
||||
const decBuf = await this.getDecryptedBuffer(response, attachment, activeUserId);
|
||||
this.fileDownloadService.download({
|
||||
fileName: attachment.fileName,
|
||||
blobData: decBuf,
|
||||
@@ -564,4 +567,41 @@ export class ViewComponent implements OnDestroy, OnInit {
|
||||
}
|
||||
this.previousCipherId = this.cipherId;
|
||||
}
|
||||
|
||||
private async getDecryptedBuffer(
|
||||
response: Response,
|
||||
attachment: AttachmentView,
|
||||
userId: UserId,
|
||||
): Promise<Uint8Array> {
|
||||
const useSdkDecryption = await this.configService.getFeatureFlag(
|
||||
FeatureFlag.PM19941MigrateCipherDomainToSdk,
|
||||
);
|
||||
|
||||
if (useSdkDecryption) {
|
||||
const ciphersData = await firstValueFrom(this.cipherService.ciphers$(userId));
|
||||
const cipherDomain = new Cipher(ciphersData[this.cipher.id as CipherId]);
|
||||
const attachmentDomain = cipherDomain.attachments?.find((a) => a.id === attachment.id);
|
||||
|
||||
const encArrayBuf = new Uint8Array(await response.arrayBuffer());
|
||||
return await this.cipherEncryptionService.decryptAttachmentContent(
|
||||
cipherDomain,
|
||||
attachmentDomain,
|
||||
encArrayBuf,
|
||||
userId,
|
||||
);
|
||||
}
|
||||
|
||||
const encBuf = await EncArrayBuffer.fromResponse(response);
|
||||
const key =
|
||||
attachment.key != null
|
||||
? attachment.key
|
||||
: await firstValueFrom(
|
||||
this.keyService
|
||||
.orgKeys$(userId)
|
||||
.pipe(
|
||||
map((orgKeys) => orgKeys[this.cipher.organizationId as OrganizationId] as OrgKey),
|
||||
),
|
||||
);
|
||||
return await this.encryptService.decryptToBytes(encBuf, key);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { CipherListView } from "@bitwarden/sdk-internal";
|
||||
|
||||
import { UserId } from "../../types/guid";
|
||||
import { Attachment } from "../models/domain/attachment";
|
||||
import { Cipher } from "../models/domain/cipher";
|
||||
import { CipherView } from "../models/view/cipher.view";
|
||||
|
||||
@@ -26,4 +27,20 @@ export abstract class CipherEncryptionService {
|
||||
* @returns A promise that resolves to an array of decrypted cipher list views
|
||||
*/
|
||||
abstract decryptCipherList(ciphers: Cipher[], userId: UserId): Promise<CipherListView[]>;
|
||||
/**
|
||||
* Decrypts an array buffer using the SDK for the given userId.
|
||||
*
|
||||
* @param cipher The encrypted cipher object that owns the attachment
|
||||
* @param attachment The encrypted attachment object
|
||||
* @param encryptedContent The encrypted content as a Uint8Array
|
||||
* @param userId The user ID whose key will be used for decryption
|
||||
*
|
||||
* @returns A promise that resolves to the decrypted content
|
||||
*/
|
||||
abstract decryptAttachmentContent(
|
||||
cipher: Cipher,
|
||||
attachment: Attachment,
|
||||
encryptedContent: Uint8Array,
|
||||
userId: UserId,
|
||||
): Promise<Uint8Array>;
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@ import {
|
||||
Cipher as SdkCipher,
|
||||
CipherType as SdkCipherType,
|
||||
CipherView as SdkCipherView,
|
||||
Attachment as SdkAttachment,
|
||||
} from "@bitwarden/sdk-internal";
|
||||
|
||||
import { mockEnc } from "../../../spec";
|
||||
@@ -16,6 +17,7 @@ import { UserId } from "../../types/guid";
|
||||
import { CipherRepromptType, CipherType } from "../enums";
|
||||
import { CipherPermissionsApi } from "../models/api/cipher-permissions.api";
|
||||
import { CipherData } from "../models/data/cipher.data";
|
||||
import { Attachment } from "../models/domain/attachment";
|
||||
import { Cipher } from "../models/domain/cipher";
|
||||
import { CipherView } from "../models/view/cipher.view";
|
||||
import { Fido2CredentialView } from "../models/view/fido2-credential.view";
|
||||
@@ -82,6 +84,9 @@ describe("DefaultCipherEncryptionService", () => {
|
||||
decrypt: jest.fn(),
|
||||
decrypt_fido2_credentials: jest.fn(),
|
||||
}),
|
||||
attachments: jest.fn().mockReturnValue({
|
||||
decrypt_buffer: jest.fn(),
|
||||
}),
|
||||
}),
|
||||
};
|
||||
const mockRef = {
|
||||
@@ -232,4 +237,34 @@ describe("DefaultCipherEncryptionService", () => {
|
||||
expect(result.name).toBe("[error: cannot decrypt]");
|
||||
});
|
||||
});
|
||||
|
||||
describe("decryptAttachmentContent", () => {
|
||||
it("should decrypt attachment content successfully", async () => {
|
||||
const cipher = new Cipher(cipherData);
|
||||
const attachment = new Attachment(cipherData.attachments![0]);
|
||||
const encryptedContent = new Uint8Array([1, 2, 3, 4]);
|
||||
const expectedDecryptedContent = new Uint8Array([5, 6, 7, 8]);
|
||||
|
||||
jest.spyOn(cipher, "toSdkCipher").mockReturnValue({ id: "id" } as SdkCipher);
|
||||
jest.spyOn(attachment, "toSdkAttachment").mockReturnValue({ id: "a1" } as SdkAttachment);
|
||||
|
||||
mockSdkClient.vault().attachments().decrypt_buffer.mockReturnValue(expectedDecryptedContent);
|
||||
|
||||
const result = await cipherEncryptionService.decryptAttachmentContent(
|
||||
cipher,
|
||||
attachment,
|
||||
encryptedContent,
|
||||
userId,
|
||||
);
|
||||
|
||||
expect(result).toEqual(expectedDecryptedContent);
|
||||
expect(cipher.toSdkCipher).toHaveBeenCalled();
|
||||
expect(attachment.toSdkAttachment).toHaveBeenCalled();
|
||||
expect(mockSdkClient.vault().attachments().decrypt_buffer).toHaveBeenCalledWith(
|
||||
{ id: "id" },
|
||||
{ id: "a1" },
|
||||
encryptedContent,
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -7,6 +7,7 @@ import { SdkService } from "../../platform/abstractions/sdk/sdk.service";
|
||||
import { UserId } from "../../types/guid";
|
||||
import { CipherEncryptionService } from "../abstractions/cipher-encryption.service";
|
||||
import { CipherType } from "../enums";
|
||||
import { Attachment } from "../models/domain/attachment";
|
||||
import { Cipher } from "../models/domain/cipher";
|
||||
import { CipherView } from "../models/view/cipher.view";
|
||||
import { Fido2CredentialView } from "../models/view/fido2-credential.view";
|
||||
@@ -102,4 +103,35 @@ export class DefaultCipherEncryptionService implements CipherEncryptionService {
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
async decryptAttachmentContent(
|
||||
cipher: Cipher,
|
||||
attachment: Attachment,
|
||||
encryptedContent: Uint8Array,
|
||||
userId: UserId,
|
||||
): Promise<Uint8Array> {
|
||||
return firstValueFrom(
|
||||
this.sdkService.userClient$(userId).pipe(
|
||||
map((sdk) => {
|
||||
if (!sdk) {
|
||||
throw new Error("SDK is undefined");
|
||||
}
|
||||
|
||||
using ref = sdk.take();
|
||||
|
||||
return ref.value
|
||||
.vault()
|
||||
.attachments()
|
||||
.decrypt_buffer(cipher.toSdkCipher(), attachment.toSdkAttachment(), encryptedContent);
|
||||
}),
|
||||
catchError((error: unknown) => {
|
||||
this.logService.error(`Failed to decrypt cipher buffer: ${error}`);
|
||||
return EMPTY;
|
||||
}),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,9 +8,11 @@ import { AccountInfo, AccountService } from "@bitwarden/common/auth/abstractions
|
||||
import { CryptoFunctionService } from "@bitwarden/common/key-management/crypto/abstractions/crypto-function.service";
|
||||
import { EncryptService } from "@bitwarden/common/key-management/crypto/abstractions/encrypt.service";
|
||||
import { CipherWithIdExport } from "@bitwarden/common/models/export/cipher-with-ids.export";
|
||||
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
|
||||
import { Utils } from "@bitwarden/common/platform/misc/utils";
|
||||
import { EncryptedString, EncString } from "@bitwarden/common/platform/models/domain/enc-string";
|
||||
import { UserId } from "@bitwarden/common/types/guid";
|
||||
import { CipherId, UserId } from "@bitwarden/common/types/guid";
|
||||
import { CipherEncryptionService } from "@bitwarden/common/vault/abstractions/cipher-encryption.service";
|
||||
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
|
||||
import { FolderService } from "@bitwarden/common/vault/abstractions/folder/folder.service.abstraction";
|
||||
import { CipherType } from "@bitwarden/common/vault/enums";
|
||||
@@ -170,8 +172,12 @@ describe("VaultExportService", () => {
|
||||
let kdfConfigService: MockProxy<KdfConfigService>;
|
||||
let accountService: MockProxy<AccountService>;
|
||||
let apiService: MockProxy<ApiService>;
|
||||
let configService: MockProxy<ConfigService>;
|
||||
let cipherEncryptionService: MockProxy<CipherEncryptionService>;
|
||||
let fetchMock: jest.Mock;
|
||||
|
||||
const userId = "" as UserId;
|
||||
|
||||
beforeEach(() => {
|
||||
cryptoFunctionService = mock<CryptoFunctionService>();
|
||||
cipherService = mock<CipherService>();
|
||||
@@ -182,10 +188,11 @@ describe("VaultExportService", () => {
|
||||
kdfConfigService = mock<KdfConfigService>();
|
||||
accountService = mock<AccountService>();
|
||||
apiService = mock<ApiService>();
|
||||
configService = mock<ConfigService>();
|
||||
cipherEncryptionService = mock<CipherEncryptionService>();
|
||||
|
||||
keyService.userKey$.mockReturnValue(new BehaviorSubject("mockOriginalUserKey" as any));
|
||||
|
||||
const userId = "" as UserId;
|
||||
const accountInfo: AccountInfo = {
|
||||
email: "",
|
||||
emailVerified: true,
|
||||
@@ -211,6 +218,7 @@ describe("VaultExportService", () => {
|
||||
kdfConfigService.getKdfConfig.mockResolvedValue(DEFAULT_KDF_CONFIG);
|
||||
encryptService.encrypt.mockResolvedValue(new EncString("encrypted"));
|
||||
apiService.getAttachmentData.mockResolvedValue(attachmentResponse);
|
||||
configService.getFeatureFlag.mockResolvedValue(false);
|
||||
|
||||
exportService = new IndividualVaultExportService(
|
||||
folderService,
|
||||
@@ -222,6 +230,8 @@ describe("VaultExportService", () => {
|
||||
kdfConfigService,
|
||||
accountService,
|
||||
apiService,
|
||||
configService,
|
||||
cipherEncryptionService,
|
||||
);
|
||||
});
|
||||
|
||||
@@ -379,6 +389,47 @@ describe("VaultExportService", () => {
|
||||
const attachment = await zip.file("attachments/mock-id/mock-file-name")?.async("blob");
|
||||
expect(attachment).toBeDefined();
|
||||
});
|
||||
|
||||
it("calls decryptAttachmentContent when SDK flag is enabled", async () => {
|
||||
const attachmentData = new AttachmentData();
|
||||
attachmentData.id = "mock-id";
|
||||
const cipherData = new CipherData();
|
||||
cipherData.id = "mock-id";
|
||||
cipherData.attachments = [attachmentData];
|
||||
const cipherRecord: Record<CipherId, CipherData> = {
|
||||
["mock-id" as CipherId]: cipherData,
|
||||
};
|
||||
const cipherView = new CipherView(new Cipher(cipherData));
|
||||
const attachmentView = new AttachmentView(new Attachment(attachmentData));
|
||||
attachmentView.fileName = "mock-file-name";
|
||||
cipherView.attachments = [attachmentView];
|
||||
cipherService.getAllDecrypted.mockResolvedValue([cipherView]);
|
||||
cipherService.ciphers$.mockReturnValue(of(cipherRecord));
|
||||
cipherEncryptionService.decryptAttachmentContent.mockResolvedValue(new Uint8Array());
|
||||
folderService.getAllDecryptedFromState.mockResolvedValue([]);
|
||||
configService.getFeatureFlag.mockResolvedValue(true);
|
||||
global.fetch = jest.fn(() =>
|
||||
Promise.resolve({
|
||||
status: 200,
|
||||
arrayBuffer: () => Promise.resolve(new ArrayBuffer(255)),
|
||||
}),
|
||||
) as any;
|
||||
global.Request = jest.fn(() => {}) as any;
|
||||
|
||||
const exportedVault = await exportService.getExport("zip");
|
||||
|
||||
expect(cipherEncryptionService.decryptAttachmentContent).toHaveBeenCalledWith(
|
||||
expect.any(Cipher),
|
||||
expect.any(Attachment),
|
||||
expect.any(Uint8Array),
|
||||
userId,
|
||||
);
|
||||
expect(exportedVault.type).toBe("application/zip");
|
||||
const exportZip = exportedVault as ExportedVaultAsBlob;
|
||||
const zip = await JSZip.loadAsync(exportZip.data);
|
||||
const attachment = await zip.file("attachments/mock-id/mock-file-name")?.async("blob");
|
||||
expect(attachment).toBeDefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe("password protected export", () => {
|
||||
|
||||
@@ -8,11 +8,15 @@ import { PinServiceAbstraction } from "@bitwarden/auth/common";
|
||||
import { ApiService } from "@bitwarden/common/abstractions/api.service";
|
||||
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||
import { getUserId } from "@bitwarden/common/auth/services/account.service";
|
||||
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
|
||||
import { CryptoFunctionService } from "@bitwarden/common/key-management/crypto/abstractions/crypto-function.service";
|
||||
import { EncryptService } from "@bitwarden/common/key-management/crypto/abstractions/encrypt.service";
|
||||
import { CipherWithIdExport, FolderWithIdExport } from "@bitwarden/common/models/export";
|
||||
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
|
||||
import { Utils } from "@bitwarden/common/platform/misc/utils";
|
||||
import { EncArrayBuffer } from "@bitwarden/common/platform/models/domain/enc-array-buffer";
|
||||
import { CipherId, UserId } from "@bitwarden/common/types/guid";
|
||||
import { CipherEncryptionService } from "@bitwarden/common/vault/abstractions/cipher-encryption.service";
|
||||
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
|
||||
import { FolderService } from "@bitwarden/common/vault/abstractions/folder/folder.service.abstraction";
|
||||
import { CipherType } from "@bitwarden/common/vault/enums";
|
||||
@@ -51,6 +55,8 @@ export class IndividualVaultExportService
|
||||
kdfConfigService: KdfConfigService,
|
||||
private accountService: AccountService,
|
||||
private apiService: ApiService,
|
||||
private configService: ConfigService,
|
||||
private cipherEncryptionService: CipherEncryptionService,
|
||||
) {
|
||||
super(pinService, encryptService, cryptoFunctionService, kdfConfigService);
|
||||
}
|
||||
@@ -116,7 +122,7 @@ export class IndividualVaultExportService
|
||||
const cipherFolder = attachmentsFolder.folder(cipher.id);
|
||||
for (const attachment of cipher.attachments) {
|
||||
const response = await this.downloadAttachment(cipher.id, attachment.id);
|
||||
const decBuf = await this.decryptAttachment(cipher, attachment, response);
|
||||
const decBuf = await this.decryptAttachment(cipher, attachment, response, activeUserId);
|
||||
cipherFolder.file(attachment.fileName, decBuf);
|
||||
}
|
||||
}
|
||||
@@ -148,8 +154,27 @@ export class IndividualVaultExportService
|
||||
cipher: CipherView,
|
||||
attachment: AttachmentView,
|
||||
response: Response,
|
||||
) {
|
||||
userId: UserId,
|
||||
): Promise<Uint8Array> {
|
||||
try {
|
||||
const useSdkDecryption = await this.configService.getFeatureFlag(
|
||||
FeatureFlag.PM19941MigrateCipherDomainToSdk,
|
||||
);
|
||||
|
||||
if (useSdkDecryption) {
|
||||
const ciphersData = await firstValueFrom(this.cipherService.ciphers$(userId));
|
||||
const cipherDomain = new Cipher(ciphersData[cipher.id as CipherId]);
|
||||
const attachmentDomain = cipherDomain.attachments?.find((a) => a.id === attachment.id);
|
||||
|
||||
const encArrayBuf = new Uint8Array(await response.arrayBuffer());
|
||||
return await this.cipherEncryptionService.decryptAttachmentContent(
|
||||
cipherDomain,
|
||||
attachmentDomain,
|
||||
encArrayBuf,
|
||||
userId,
|
||||
);
|
||||
}
|
||||
|
||||
const encBuf = await EncArrayBuffer.fromResponse(response);
|
||||
const key =
|
||||
attachment.key != null
|
||||
|
||||
@@ -7,9 +7,11 @@ import { AccountInfo, AccountService } from "@bitwarden/common/auth/abstractions
|
||||
import { CryptoFunctionService } from "@bitwarden/common/key-management/crypto/abstractions/crypto-function.service";
|
||||
import { EncryptService } from "@bitwarden/common/key-management/crypto/abstractions/encrypt.service";
|
||||
import { CipherWithIdExport } from "@bitwarden/common/models/export/cipher-with-ids.export";
|
||||
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
|
||||
import { Utils } from "@bitwarden/common/platform/misc/utils";
|
||||
import { EncryptedString, EncString } from "@bitwarden/common/platform/models/domain/enc-string";
|
||||
import { UserId } from "@bitwarden/common/types/guid";
|
||||
import { CipherEncryptionService } from "@bitwarden/common/vault/abstractions/cipher-encryption.service";
|
||||
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
|
||||
import { FolderService } from "@bitwarden/common/vault/abstractions/folder/folder.service.abstraction";
|
||||
import { CipherType } from "@bitwarden/common/vault/enums";
|
||||
@@ -159,6 +161,8 @@ describe("VaultExportService", () => {
|
||||
let accountService: MockProxy<AccountService>;
|
||||
let kdfConfigService: MockProxy<KdfConfigService>;
|
||||
let apiService: MockProxy<ApiService>;
|
||||
let configService: MockProxy<ConfigService>;
|
||||
let cipherEncryptionService: MockProxy<CipherEncryptionService>;
|
||||
|
||||
beforeEach(() => {
|
||||
cryptoFunctionService = mock<CryptoFunctionService>();
|
||||
@@ -169,6 +173,8 @@ describe("VaultExportService", () => {
|
||||
encryptService = mock<EncryptService>();
|
||||
accountService = mock<AccountService>();
|
||||
apiService = mock<ApiService>();
|
||||
configService = mock<ConfigService>();
|
||||
cipherEncryptionService = mock<CipherEncryptionService>();
|
||||
|
||||
kdfConfigService = mock<KdfConfigService>();
|
||||
|
||||
@@ -196,6 +202,8 @@ describe("VaultExportService", () => {
|
||||
kdfConfigService,
|
||||
accountService,
|
||||
apiService,
|
||||
configService,
|
||||
cipherEncryptionService,
|
||||
);
|
||||
});
|
||||
|
||||
|
||||
@@ -4,12 +4,16 @@ import { mock } from "jest-mock-extended";
|
||||
import { BehaviorSubject } from "rxjs";
|
||||
|
||||
import { ApiService } from "@bitwarden/common/abstractions/api.service";
|
||||
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
|
||||
import { EncryptService } from "@bitwarden/common/key-management/crypto/abstractions/encrypt.service";
|
||||
import { ErrorResponse } from "@bitwarden/common/models/response/error.response";
|
||||
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 { EncArrayBuffer } from "@bitwarden/common/platform/models/domain/enc-array-buffer";
|
||||
import { StateProvider } from "@bitwarden/common/platform/state";
|
||||
import { CipherEncryptionService } from "@bitwarden/common/vault/abstractions/cipher-encryption.service";
|
||||
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
|
||||
import { CipherType } from "@bitwarden/common/vault/enums";
|
||||
import { AttachmentView } from "@bitwarden/common/vault/models/view/attachment.view";
|
||||
import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
|
||||
@@ -51,6 +55,21 @@ describe("DownloadAttachmentComponent", () => {
|
||||
},
|
||||
} as CipherView;
|
||||
|
||||
const ciphers$ = new BehaviorSubject({
|
||||
"5555-444-3333": {
|
||||
id: "5555-444-3333",
|
||||
attachments: [
|
||||
{
|
||||
id: "222-3333-4444",
|
||||
fileName: "encrypted-filename",
|
||||
key: "encrypted-key",
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
|
||||
const getFeatureFlag = jest.fn().mockResolvedValue(false);
|
||||
|
||||
beforeEach(async () => {
|
||||
showToast.mockClear();
|
||||
getAttachmentData.mockClear();
|
||||
@@ -67,6 +86,14 @@ describe("DownloadAttachmentComponent", () => {
|
||||
{ provide: ApiService, useValue: { getAttachmentData } },
|
||||
{ provide: FileDownloadService, useValue: { download } },
|
||||
{ provide: PasswordRepromptService, useValue: mock<PasswordRepromptService>() },
|
||||
{
|
||||
provide: ConfigService,
|
||||
useValue: {
|
||||
getFeatureFlag,
|
||||
},
|
||||
},
|
||||
{ provide: CipherService, useValue: { ciphers$: () => ciphers$ } },
|
||||
{ provide: CipherEncryptionService, useValue: mock<CipherEncryptionService>() },
|
||||
],
|
||||
}).compileComponents();
|
||||
});
|
||||
@@ -114,6 +141,29 @@ describe("DownloadAttachmentComponent", () => {
|
||||
expect(download).toHaveBeenCalledWith({ blobData: undefined, fileName: attachment.fileName });
|
||||
});
|
||||
|
||||
it("calls file download service with SDK decryption when SDK flag is enabled", async () => {
|
||||
getFeatureFlag.mockResolvedValue(true);
|
||||
getAttachmentData.mockResolvedValue({ url: "https://www.downloadattachement.com" });
|
||||
fetchMock.mockResolvedValue({
|
||||
status: 200,
|
||||
arrayBuffer: jest.fn().mockResolvedValue(new ArrayBuffer(8)),
|
||||
});
|
||||
|
||||
const cipherEncryptionService = TestBed.inject(CipherEncryptionService);
|
||||
const decryptAttachmentContent = jest
|
||||
.spyOn(cipherEncryptionService, "decryptAttachmentContent")
|
||||
.mockResolvedValue(new Uint8Array());
|
||||
|
||||
await component.download();
|
||||
|
||||
expect(getFeatureFlag).toHaveBeenCalledWith(FeatureFlag.PM19941MigrateCipherDomainToSdk);
|
||||
expect(decryptAttachmentContent).toHaveBeenCalled();
|
||||
expect(download).toHaveBeenCalledWith({
|
||||
blobData: new Uint8Array(),
|
||||
fileName: attachment.fileName,
|
||||
});
|
||||
});
|
||||
|
||||
describe("errors", () => {
|
||||
it("shows an error toast when fetch fails", async () => {
|
||||
getAttachmentData.mockResolvedValue({ url: "https://www.downloadattachement.com" });
|
||||
|
||||
@@ -3,18 +3,23 @@
|
||||
import { CommonModule } from "@angular/common";
|
||||
import { Component, Input } from "@angular/core";
|
||||
import { takeUntilDestroyed } from "@angular/core/rxjs-interop";
|
||||
import { NEVER, switchMap } from "rxjs";
|
||||
import { NEVER, firstValueFrom, switchMap } from "rxjs";
|
||||
|
||||
import { JslibModule } from "@bitwarden/angular/jslib.module";
|
||||
import { ApiService } from "@bitwarden/common/abstractions/api.service";
|
||||
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
|
||||
import { EncryptService } from "@bitwarden/common/key-management/crypto/abstractions/encrypt.service";
|
||||
import { ErrorResponse } from "@bitwarden/common/models/response/error.response";
|
||||
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 { EncArrayBuffer } from "@bitwarden/common/platform/models/domain/enc-array-buffer";
|
||||
import { StateProvider } from "@bitwarden/common/platform/state";
|
||||
import { EmergencyAccessId, OrganizationId } from "@bitwarden/common/types/guid";
|
||||
import { CipherId, EmergencyAccessId, OrganizationId } from "@bitwarden/common/types/guid";
|
||||
import { OrgKey } from "@bitwarden/common/types/key";
|
||||
import { CipherEncryptionService } from "@bitwarden/common/vault/abstractions/cipher-encryption.service";
|
||||
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
|
||||
import { Cipher } from "@bitwarden/common/vault/models/domain/cipher";
|
||||
import { AttachmentView } from "@bitwarden/common/vault/models/view/attachment.view";
|
||||
import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
|
||||
import { AsyncActionsModule, IconButtonModule, ToastService } from "@bitwarden/components";
|
||||
@@ -50,6 +55,9 @@ export class DownloadAttachmentComponent {
|
||||
private encryptService: EncryptService,
|
||||
private stateProvider: StateProvider,
|
||||
private keyService: KeyService,
|
||||
private configService: ConfigService,
|
||||
private cipherService: CipherService,
|
||||
private cipherEncryptionService: CipherEncryptionService,
|
||||
) {
|
||||
this.stateProvider.activeUserId$
|
||||
.pipe(
|
||||
@@ -95,9 +103,8 @@ export class DownloadAttachmentComponent {
|
||||
}
|
||||
|
||||
try {
|
||||
const encBuf = await EncArrayBuffer.fromResponse(response);
|
||||
const key = this.attachment.key != null ? this.attachment.key : this.orgKey;
|
||||
const decBuf = await this.encryptService.decryptToBytes(encBuf, key);
|
||||
const decBuf = await this.getDecryptedBuffer(response);
|
||||
|
||||
this.fileDownloadService.download({
|
||||
fileName: this.attachment.fileName,
|
||||
blobData: decBuf,
|
||||
@@ -112,4 +119,29 @@ export class DownloadAttachmentComponent {
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
private async getDecryptedBuffer(response: Response): Promise<Uint8Array> {
|
||||
const useSdkDecryption = await this.configService.getFeatureFlag(
|
||||
FeatureFlag.PM19941MigrateCipherDomainToSdk,
|
||||
);
|
||||
|
||||
if (useSdkDecryption) {
|
||||
const userId = await firstValueFrom(this.stateProvider.activeUserId$);
|
||||
const ciphersData = await firstValueFrom(this.cipherService.ciphers$(userId));
|
||||
const cipherDomain = new Cipher(ciphersData[this.cipher.id as CipherId]);
|
||||
const attachmentDomain = cipherDomain.attachments?.find((a) => a.id === this.attachment.id);
|
||||
|
||||
const encArrayBuf = new Uint8Array(await response.arrayBuffer());
|
||||
return await this.cipherEncryptionService.decryptAttachmentContent(
|
||||
cipherDomain,
|
||||
attachmentDomain,
|
||||
encArrayBuf,
|
||||
userId,
|
||||
);
|
||||
}
|
||||
|
||||
const encBuf = await EncArrayBuffer.fromResponse(response);
|
||||
const key = this.attachment.key != null ? this.attachment.key : this.orgKey;
|
||||
return await this.encryptService.decryptToBytes(encBuf, key);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user