mirror of
https://github.com/bitwarden/browser
synced 2025-12-16 08:13:42 +00:00
[SG 623] Send Service Refactor (#4327)
* Split out api methods into sendApiService * Move SendService and abstraction * Libs updates * Web updates * CLI updates * Desktop updates * libs send service fixes * browser factory additions * Browser updates * Fix service injection for CLI SendReceiveCommand * Deprecate directly calling send state service methods * SendService observables updates * Update components to use new observables * Modify CLI to use state service instead of observables * Remove unnecessary await on get() * Move delete() to InternalSendService * SendService unit tests * Split fileUploadService by send and cipher * send and cipher service factory updates * Add file upload methods to get around circular dependency issues * Move api methods from sendService to sendApiService * Update cipherService to use fileApi methods * libs service injection and component changes * browser service injection and component changes * Desktop component changes * Web component changes * cipher service test fix * Fix file capitalization * CLI service import and command updates * Remove extra abstract fileUploadService * WIP: Condense callbacks for file upload Co-authored-by: Robyn MacCallum <robyntmaccallum@gmail.com> * Send callbacks for file upload * Fix circular service dependencies * Fix response return on upload * Fix function definitions * Service injection fixes and bug fixes * Fix folder casing * Service injection cleanup * Remove deleted file from capital letters whitelist * Create new SendApiService for popup * Move cipherFileUploadService to vault * Move SendFileUploadService methods into SendApiService * Rename methods to remove 'WithServer' * Properly subscribe to sendViews * Fix Send serialization * Implement fromJSON on sendFile and sendText * [PM-1347] Fix send key serialization (#4989) * Properly serialize key on send fromJSON * Remove call that nulled out decrypted sends * Fix null checks in fromJSON methods for models * lint fixes --------- Co-authored-by: Matt Gibson <mgibson@bitwarden.com>
This commit is contained in:
@@ -0,0 +1,15 @@
|
||||
import { EncArrayBuffer } from "../../../models/domain/enc-array-buffer";
|
||||
import { EncString } from "../../../models/domain/enc-string";
|
||||
import { SymmetricCryptoKey } from "../../../models/domain/symmetric-crypto-key";
|
||||
import { Cipher } from "../../models/domain/cipher";
|
||||
import { CipherResponse } from "../../models/response/cipher.response";
|
||||
|
||||
export abstract class CipherFileUploadService {
|
||||
upload: (
|
||||
cipher: Cipher,
|
||||
encFileName: EncString,
|
||||
encData: EncArrayBuffer,
|
||||
admin: boolean,
|
||||
dataEncKey: [SymmetricCryptoKey, EncString]
|
||||
) => Promise<CipherResponse>;
|
||||
}
|
||||
@@ -4,7 +4,6 @@ import { Arg, Substitute, SubstituteOf } from "@fluffy-spoon/substitute";
|
||||
import { ApiService } from "../../abstractions/api.service";
|
||||
import { CryptoService } from "../../abstractions/crypto.service";
|
||||
import { EncryptService } from "../../abstractions/encrypt.service";
|
||||
import { FileUploadService } from "../../abstractions/fileUpload.service";
|
||||
import { I18nService } from "../../abstractions/i18n.service";
|
||||
import { LogService } from "../../abstractions/log.service";
|
||||
import { SearchService } from "../../abstractions/search.service";
|
||||
@@ -13,6 +12,7 @@ import { StateService } from "../../abstractions/state.service";
|
||||
import { EncArrayBuffer } from "../../models/domain/enc-array-buffer";
|
||||
import { EncString } from "../../models/domain/enc-string";
|
||||
import { SymmetricCryptoKey } from "../../models/domain/symmetric-crypto-key";
|
||||
import { CipherFileUploadService } from "../abstractions/file-upload/cipher-file-upload.service";
|
||||
import { Cipher } from "../models/domain/cipher";
|
||||
|
||||
import { CipherService } from "./cipher.service";
|
||||
@@ -25,7 +25,7 @@ describe("Cipher Service", () => {
|
||||
let stateService: SubstituteOf<StateService>;
|
||||
let settingsService: SubstituteOf<SettingsService>;
|
||||
let apiService: SubstituteOf<ApiService>;
|
||||
let fileUploadService: SubstituteOf<FileUploadService>;
|
||||
let cipherFileUploadService: SubstituteOf<CipherFileUploadService>;
|
||||
let i18nService: SubstituteOf<I18nService>;
|
||||
let searchService: SubstituteOf<SearchService>;
|
||||
let logService: SubstituteOf<LogService>;
|
||||
@@ -38,7 +38,7 @@ describe("Cipher Service", () => {
|
||||
stateService = Substitute.for<StateService>();
|
||||
settingsService = Substitute.for<SettingsService>();
|
||||
apiService = Substitute.for<ApiService>();
|
||||
fileUploadService = Substitute.for<FileUploadService>();
|
||||
cipherFileUploadService = Substitute.for<CipherFileUploadService>();
|
||||
i18nService = Substitute.for<I18nService>();
|
||||
searchService = Substitute.for<SearchService>();
|
||||
logService = Substitute.for<LogService>();
|
||||
@@ -51,12 +51,12 @@ describe("Cipher Service", () => {
|
||||
cryptoService,
|
||||
settingsService,
|
||||
apiService,
|
||||
fileUploadService,
|
||||
i18nService,
|
||||
() => searchService,
|
||||
logService,
|
||||
stateService,
|
||||
encryptService
|
||||
encryptService,
|
||||
cipherFileUploadService
|
||||
);
|
||||
});
|
||||
|
||||
@@ -67,8 +67,8 @@ describe("Cipher Service", () => {
|
||||
|
||||
await cipherService.saveAttachmentRawWithServer(new Cipher(), fileName, fileData);
|
||||
|
||||
fileUploadService
|
||||
cipherFileUploadService
|
||||
.received(1)
|
||||
.uploadCipherAttachment(Arg.any(), Arg.any(), new EncString(ENCRYPTED_TEXT), ENCRYPTED_BYTES);
|
||||
.upload(Arg.any(), Arg.any(), ENCRYPTED_BYTES, Arg.any(), Arg.any());
|
||||
});
|
||||
});
|
||||
|
||||
@@ -3,7 +3,6 @@ import { firstValueFrom } from "rxjs";
|
||||
import { ApiService } from "../../abstractions/api.service";
|
||||
import { CryptoService } from "../../abstractions/crypto.service";
|
||||
import { EncryptService } from "../../abstractions/encrypt.service";
|
||||
import { FileUploadService } from "../../abstractions/fileUpload.service";
|
||||
import { I18nService } from "../../abstractions/i18n.service";
|
||||
import { LogService } from "../../abstractions/log.service";
|
||||
import { SearchService } from "../../abstractions/search.service";
|
||||
@@ -21,6 +20,7 @@ import { SymmetricCryptoKey } from "../../models/domain/symmetric-crypto-key";
|
||||
import { ErrorResponse } from "../../models/response/error.response";
|
||||
import { View } from "../../models/view/view";
|
||||
import { CipherService as CipherServiceAbstraction } from "../abstractions/cipher.service";
|
||||
import { CipherFileUploadService } from "../abstractions/file-upload/cipher-file-upload.service";
|
||||
import { CipherType } from "../enums/cipher-type";
|
||||
import { CipherData } from "../models/data/cipher.data";
|
||||
import { Attachment } from "../models/domain/attachment";
|
||||
@@ -33,7 +33,6 @@ import { LoginUri } from "../models/domain/login-uri";
|
||||
import { Password } from "../models/domain/password";
|
||||
import { SecureNote } from "../models/domain/secure-note";
|
||||
import { SortedCiphersCache } from "../models/domain/sorted-ciphers-cache";
|
||||
import { AttachmentRequest } from "../models/request/attachment.request";
|
||||
import { CipherBulkDeleteRequest } from "../models/request/cipher-bulk-delete.request";
|
||||
import { CipherBulkMoveRequest } from "../models/request/cipher-bulk-move.request";
|
||||
import { CipherBulkRestoreRequest } from "../models/request/cipher-bulk-restore.request";
|
||||
@@ -58,12 +57,12 @@ export class CipherService implements CipherServiceAbstraction {
|
||||
private cryptoService: CryptoService,
|
||||
private settingsService: SettingsService,
|
||||
private apiService: ApiService,
|
||||
private fileUploadService: FileUploadService,
|
||||
private i18nService: I18nService,
|
||||
private searchService: () => SearchService,
|
||||
private logService: LogService,
|
||||
private stateService: StateService,
|
||||
private encryptService: EncryptService
|
||||
private encryptService: EncryptService,
|
||||
private cipherFileUploadService: CipherFileUploadService
|
||||
) {}
|
||||
|
||||
async getDecryptedCipherCache(): Promise<CipherView[]> {
|
||||
@@ -725,41 +724,13 @@ export class CipherService implements CipherServiceAbstraction {
|
||||
const dataEncKey = await this.cryptoService.makeEncKey(key);
|
||||
const encData = await this.cryptoService.encryptToBytes(data, dataEncKey[0]);
|
||||
|
||||
const request: AttachmentRequest = {
|
||||
key: dataEncKey[1].encryptedString,
|
||||
fileName: encFileName.encryptedString,
|
||||
fileSize: encData.buffer.byteLength,
|
||||
adminRequest: admin,
|
||||
};
|
||||
|
||||
let response: CipherResponse;
|
||||
try {
|
||||
const uploadDataResponse = await this.apiService.postCipherAttachment(cipher.id, request);
|
||||
response = admin ? uploadDataResponse.cipherMiniResponse : uploadDataResponse.cipherResponse;
|
||||
await this.fileUploadService.uploadCipherAttachment(
|
||||
admin,
|
||||
uploadDataResponse,
|
||||
encFileName,
|
||||
encData
|
||||
);
|
||||
} catch (e) {
|
||||
if (
|
||||
(e instanceof ErrorResponse && (e as ErrorResponse).statusCode === 404) ||
|
||||
(e as ErrorResponse).statusCode === 405
|
||||
) {
|
||||
response = await this.legacyServerAttachmentFileUpload(
|
||||
admin,
|
||||
cipher.id,
|
||||
encFileName,
|
||||
encData,
|
||||
dataEncKey[1]
|
||||
);
|
||||
} else if (e instanceof ErrorResponse) {
|
||||
throw new Error((e as ErrorResponse).getSingleMessage());
|
||||
} else {
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
const response = await this.cipherFileUploadService.upload(
|
||||
cipher,
|
||||
encFileName,
|
||||
encData,
|
||||
admin,
|
||||
dataEncKey
|
||||
);
|
||||
|
||||
const cData = new CipherData(response, cipher.collectionIds);
|
||||
if (!admin) {
|
||||
@@ -768,52 +739,6 @@ export class CipherService implements CipherServiceAbstraction {
|
||||
return new Cipher(cData);
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated Mar 25 2021: This method has been deprecated in favor of direct uploads.
|
||||
* This method still exists for backward compatibility with old server versions.
|
||||
*/
|
||||
async legacyServerAttachmentFileUpload(
|
||||
admin: boolean,
|
||||
cipherId: string,
|
||||
encFileName: EncString,
|
||||
encData: EncArrayBuffer,
|
||||
key: EncString
|
||||
) {
|
||||
const fd = new FormData();
|
||||
try {
|
||||
const blob = new Blob([encData.buffer], { type: "application/octet-stream" });
|
||||
fd.append("key", key.encryptedString);
|
||||
fd.append("data", blob, encFileName.encryptedString);
|
||||
} catch (e) {
|
||||
if (Utils.isNode && !Utils.isBrowser) {
|
||||
fd.append("key", key.encryptedString);
|
||||
fd.append(
|
||||
"data",
|
||||
Buffer.from(encData.buffer) as any,
|
||||
{
|
||||
filepath: encFileName.encryptedString,
|
||||
contentType: "application/octet-stream",
|
||||
} as any
|
||||
);
|
||||
} else {
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
let response: CipherResponse;
|
||||
try {
|
||||
if (admin) {
|
||||
response = await this.apiService.postCipherAttachmentAdminLegacy(cipherId, fd);
|
||||
} else {
|
||||
response = await this.apiService.postCipherAttachmentLegacy(cipherId, fd);
|
||||
}
|
||||
} catch (e) {
|
||||
throw new Error((e as ErrorResponse).getSingleMessage());
|
||||
}
|
||||
|
||||
return response;
|
||||
}
|
||||
|
||||
async saveCollectionsWithServer(cipher: Cipher): Promise<any> {
|
||||
const request = new CipherCollectionsRequest(cipher.collectionIds);
|
||||
await this.apiService.putCipherCollections(cipher.id, request);
|
||||
|
||||
@@ -0,0 +1,157 @@
|
||||
import { ApiService } from "../../../abstractions/api.service";
|
||||
import {
|
||||
FileUploadApiMethods,
|
||||
FileUploadService,
|
||||
} from "../../../abstractions/file-upload/file-upload.service";
|
||||
import { Utils } from "../../../misc/utils";
|
||||
import { EncArrayBuffer } from "../../../models/domain/enc-array-buffer";
|
||||
import { EncString } from "../../../models/domain/enc-string";
|
||||
import { SymmetricCryptoKey } from "../../../models/domain/symmetric-crypto-key";
|
||||
import { ErrorResponse } from "../../../models/response/error.response";
|
||||
import { CipherFileUploadService as CipherFileUploadServiceAbstraction } from "../../abstractions/file-upload/cipher-file-upload.service";
|
||||
import { Cipher } from "../../models/domain/cipher";
|
||||
import { AttachmentRequest } from "../../models/request/attachment.request";
|
||||
import { AttachmentUploadDataResponse } from "../../models/response/attachment-upload-data.response";
|
||||
import { CipherResponse } from "../../models/response/cipher.response";
|
||||
|
||||
export class CipherFileUploadService implements CipherFileUploadServiceAbstraction {
|
||||
constructor(private apiService: ApiService, private fileUploadService: FileUploadService) {}
|
||||
|
||||
async upload(
|
||||
cipher: Cipher,
|
||||
encFileName: EncString,
|
||||
encData: EncArrayBuffer,
|
||||
admin: boolean,
|
||||
dataEncKey: [SymmetricCryptoKey, EncString]
|
||||
): Promise<CipherResponse> {
|
||||
const request: AttachmentRequest = {
|
||||
key: dataEncKey[1].encryptedString,
|
||||
fileName: encFileName.encryptedString,
|
||||
fileSize: encData.buffer.byteLength,
|
||||
adminRequest: admin,
|
||||
};
|
||||
|
||||
let response: CipherResponse;
|
||||
try {
|
||||
const uploadDataResponse = await this.apiService.postCipherAttachment(cipher.id, request);
|
||||
response = admin ? uploadDataResponse.cipherMiniResponse : uploadDataResponse.cipherResponse;
|
||||
await this.fileUploadService.upload(
|
||||
uploadDataResponse,
|
||||
encFileName,
|
||||
encData,
|
||||
this.generateMethods(uploadDataResponse, response, request.adminRequest)
|
||||
);
|
||||
} catch (e) {
|
||||
if (
|
||||
(e instanceof ErrorResponse && (e as ErrorResponse).statusCode === 404) ||
|
||||
(e as ErrorResponse).statusCode === 405
|
||||
) {
|
||||
response = await this.legacyServerAttachmentFileUpload(
|
||||
request.adminRequest,
|
||||
cipher.id,
|
||||
encFileName,
|
||||
encData,
|
||||
dataEncKey[1]
|
||||
);
|
||||
} else if (e instanceof ErrorResponse) {
|
||||
throw new Error((e as ErrorResponse).getSingleMessage());
|
||||
} else {
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
return response;
|
||||
}
|
||||
|
||||
private generateMethods(
|
||||
uploadData: AttachmentUploadDataResponse,
|
||||
response: CipherResponse,
|
||||
isAdmin: boolean
|
||||
): FileUploadApiMethods {
|
||||
return {
|
||||
postDirect: this.generatePostDirectCallback(uploadData, isAdmin),
|
||||
renewFileUploadUrl: this.generateRenewFileUploadUrlCallback(uploadData, response, isAdmin),
|
||||
rollback: this.generateRollbackCallback(response, uploadData, isAdmin),
|
||||
};
|
||||
}
|
||||
|
||||
private generatePostDirectCallback(uploadData: AttachmentUploadDataResponse, isAdmin: boolean) {
|
||||
return (data: FormData) => {
|
||||
const response = isAdmin ? uploadData.cipherMiniResponse : uploadData.cipherResponse;
|
||||
return this.apiService.postAttachmentFile(response.id, uploadData.attachmentId, data);
|
||||
};
|
||||
}
|
||||
|
||||
private generateRenewFileUploadUrlCallback(
|
||||
uploadData: AttachmentUploadDataResponse,
|
||||
response: CipherResponse,
|
||||
isAdmin: boolean
|
||||
) {
|
||||
return async () => {
|
||||
const renewResponse = await this.apiService.renewAttachmentUploadUrl(
|
||||
response.id,
|
||||
uploadData.attachmentId
|
||||
);
|
||||
return renewResponse?.url;
|
||||
};
|
||||
}
|
||||
|
||||
private generateRollbackCallback(
|
||||
response: CipherResponse,
|
||||
uploadData: AttachmentUploadDataResponse,
|
||||
isAdmin: boolean
|
||||
) {
|
||||
return () => {
|
||||
if (isAdmin) {
|
||||
return this.apiService.deleteCipherAttachmentAdmin(response.id, uploadData.attachmentId);
|
||||
} else {
|
||||
return this.apiService.deleteCipherAttachment(response.id, uploadData.attachmentId);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated Mar 25 2021: This method has been deprecated in favor of direct uploads.
|
||||
* This method still exists for backward compatibility with old server versions.
|
||||
*/
|
||||
async legacyServerAttachmentFileUpload(
|
||||
admin: boolean,
|
||||
cipherId: string,
|
||||
encFileName: EncString,
|
||||
encData: EncArrayBuffer,
|
||||
key: EncString
|
||||
) {
|
||||
const fd = new FormData();
|
||||
try {
|
||||
const blob = new Blob([encData.buffer], { type: "application/octet-stream" });
|
||||
fd.append("key", key.encryptedString);
|
||||
fd.append("data", blob, encFileName.encryptedString);
|
||||
} catch (e) {
|
||||
if (Utils.isNode && !Utils.isBrowser) {
|
||||
fd.append("key", key.encryptedString);
|
||||
fd.append(
|
||||
"data",
|
||||
Buffer.from(encData.buffer) as any,
|
||||
{
|
||||
filepath: encFileName.encryptedString,
|
||||
contentType: "application/octet-stream",
|
||||
} as any
|
||||
);
|
||||
} else {
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
let response: CipherResponse;
|
||||
try {
|
||||
if (admin) {
|
||||
response = await this.apiService.postCipherAttachmentAdminLegacy(cipherId, fd);
|
||||
} else {
|
||||
response = await this.apiService.postCipherAttachmentLegacy(cipherId, fd);
|
||||
}
|
||||
} catch (e) {
|
||||
throw new Error((e as ErrorResponse).getSingleMessage());
|
||||
}
|
||||
|
||||
return response;
|
||||
}
|
||||
}
|
||||
@@ -2,7 +2,8 @@ import { ApiService } from "../../../abstractions/api.service";
|
||||
import { CryptoService } from "../../../abstractions/crypto.service";
|
||||
import { LogService } from "../../../abstractions/log.service";
|
||||
import { MessagingService } from "../../../abstractions/messaging.service";
|
||||
import { SendService } from "../../../abstractions/send.service";
|
||||
import { SendApiService } from "../../../abstractions/send/send-api.service.abstraction";
|
||||
import { InternalSendService } from "../../../abstractions/send/send.service.abstraction";
|
||||
import { SettingsService } from "../../../abstractions/settings.service";
|
||||
import { StateService } from "../../../abstractions/state.service";
|
||||
import { CollectionService } from "../../../admin-console/abstractions/collection.service";
|
||||
@@ -47,13 +48,14 @@ export class SyncService implements SyncServiceAbstraction {
|
||||
private collectionService: CollectionService,
|
||||
private messagingService: MessagingService,
|
||||
private policyService: InternalPolicyService,
|
||||
private sendService: SendService,
|
||||
private sendService: InternalSendService,
|
||||
private logService: LogService,
|
||||
private keyConnectorService: KeyConnectorService,
|
||||
private stateService: StateService,
|
||||
private providerService: ProviderService,
|
||||
private folderApiService: FolderApiServiceAbstraction,
|
||||
private organizationService: InternalOrganizationService,
|
||||
private sendApiService: SendApiService,
|
||||
private logoutCallback: (expired: boolean) => Promise<void>
|
||||
) {}
|
||||
|
||||
@@ -230,12 +232,12 @@ export class SyncService implements SyncServiceAbstraction {
|
||||
this.syncStarted();
|
||||
if (await this.stateService.getIsAuthenticated()) {
|
||||
try {
|
||||
const localSend = await this.sendService.get(notification.id);
|
||||
const localSend = this.sendService.get(notification.id);
|
||||
if (
|
||||
(!isEdit && localSend == null) ||
|
||||
(isEdit && localSend != null && localSend.revisionDate < notification.revisionDate)
|
||||
) {
|
||||
const remoteSend = await this.apiService.getSend(notification.id);
|
||||
const remoteSend = await this.sendApiService.getSend(notification.id);
|
||||
if (remoteSend != null) {
|
||||
await this.sendService.upsert(new SendData(remoteSend));
|
||||
this.messagingService.send("syncedUpsertedSend", { sendId: notification.id });
|
||||
|
||||
Reference in New Issue
Block a user