diff --git a/src/abstractions/send.service.ts b/src/abstractions/send.service.ts new file mode 100644 index 0000000000..03862de6bc --- /dev/null +++ b/src/abstractions/send.service.ts @@ -0,0 +1,22 @@ +import { SendData } from '../models/data/sendData'; + +import { Send } from '../models/domain/send'; +import { SymmetricCryptoKey } from '../models/domain/symmetricCryptoKey'; + +import { SendView } from '../models/view/sendView'; + +export abstract class SendService { + decryptedSendCache: SendView[]; + + clearCache: () => void; + encrypt: (model: SendView, file: File, password: string, key?: SymmetricCryptoKey) => Promise<[Send, ArrayBuffer]>; + get: (id: string) => Promise; + getAll: () => Promise; + getAllDecrypted: () => Promise; + saveWithServer: (sendData: [Send, ArrayBuffer]) => Promise; + upsert: (send: SendData | SendData[]) => Promise; + replace: (sends: { [id: string]: SendData; }) => Promise; + clear: (userId: string) => Promise; + delete: (id: string | string[]) => Promise; + deleteWithServer: (id: string) => Promise; +} diff --git a/src/models/response/syncResponse.ts b/src/models/response/syncResponse.ts index dafa6de554..1fb655a3b4 100644 --- a/src/models/response/syncResponse.ts +++ b/src/models/response/syncResponse.ts @@ -5,6 +5,7 @@ import { DomainsResponse } from './domainsResponse'; import { FolderResponse } from './folderResponse'; import { PolicyResponse } from './policyResponse'; import { ProfileResponse } from './profileResponse'; +import { SendResponse } from './sendResponse'; export class SyncResponse extends BaseResponse { profile?: ProfileResponse; @@ -13,6 +14,7 @@ export class SyncResponse extends BaseResponse { ciphers: CipherResponse[] = []; domains?: DomainsResponse; policies?: PolicyResponse[] = []; + sends: SendResponse[] = []; constructor(response: any) { super(response); @@ -46,5 +48,10 @@ export class SyncResponse extends BaseResponse { if (policies != null) { this.policies = policies.map((p: any) => new PolicyResponse(p)); } + + const sends = this.getResponseProperty('Sends'); + if (sends != null) { + this.sends = sends.map((s: any) => new SendResponse(s)); + } } } diff --git a/src/services/send.service.ts b/src/services/send.service.ts new file mode 100644 index 0000000000..06b866efd5 --- /dev/null +++ b/src/services/send.service.ts @@ -0,0 +1,236 @@ +import { SendData } from '../models/data/sendData'; + +import { SendRequest } from '../models/request/sendRequest'; + +import { SendResponse } from '../models/response/sendResponse'; + +import { Send } from '../models/domain/send'; +import { SendFile } from '../models/domain/sendFile'; +import { SendText } from '../models/domain/sendText'; +import { SymmetricCryptoKey } from '../models/domain/symmetricCryptoKey'; + +import { SendType } from '../enums/sendType'; + +import { SendView } from '../models/view/sendView'; + +import { ApiService } from '../abstractions/api.service'; +import { CryptoService } from '../abstractions/crypto.service'; +import { CryptoFunctionService } from '../abstractions/cryptoFunction.service'; +import { I18nService } from '../abstractions/i18n.service'; +import { SendService as SendServiceAbstraction } from '../abstractions/send.service'; +import { StorageService } from '../abstractions/storage.service'; +import { UserService } from '../abstractions/user.service'; + +import { Utils } from '../misc/utils'; + +const Keys = { + sendsPrefix: 'sends_', +}; + +export class SendService implements SendServiceAbstraction { + decryptedSendCache: SendView[]; + + constructor(private cryptoService: CryptoService, private userService: UserService, + private apiService: ApiService, private storageService: StorageService, + private i18nService: I18nService, private cryptoFunctionService: CryptoFunctionService) { } + + clearCache(): void { + this.decryptedSendCache = null; + } + + async encrypt(model: SendView, file: File, password: string, + key?: SymmetricCryptoKey): Promise<[Send, ArrayBuffer]> { + let fileData: ArrayBuffer = null; + const send = new Send(); + send.id = model.id; + send.type = model.type; + send.disabled = model.disabled; + send.maxAccessCount = model.maxAccessCount; + if (model.key == null) { + model.key = await this.cryptoFunctionService.randomBytes(16); + model.cryptoKey = await this.cryptoService.makeSendKey(model.key); + } + if (password != null) { + const passwordHash = await this.cryptoFunctionService.pbkdf2(password, model.key, 'sha256', 100000); + send.password = Utils.fromBufferToB64(passwordHash); + } + send.key = await this.cryptoService.encrypt(model.key, key); + send.name = await this.cryptoService.encrypt(model.name, model.cryptoKey); + send.notes = await this.cryptoService.encrypt(model.notes, model.cryptoKey); + if (send.type === SendType.Text) { + send.text = new SendText(); + send.text.text = await this.cryptoService.encrypt(model.text.text, model.cryptoKey); + send.text.hidden = model.text.hidden; + } else if (send.type === SendType.File) { + send.file = new SendFile(); + if (file != null) { + fileData = await this.parseFile(send, file, model.cryptoKey); + } + } + + return [send, fileData]; + } + + async get(id: string): Promise { + const userId = await this.userService.getUserId(); + const sends = await this.storageService.get<{ [id: string]: SendData; }>( + Keys.sendsPrefix + userId); + if (sends == null || !sends.hasOwnProperty(id)) { + return null; + } + + return new Send(sends[id]); + } + + async getAll(): Promise { + const userId = await this.userService.getUserId(); + const sends = await this.storageService.get<{ [id: string]: SendData; }>( + Keys.sendsPrefix + userId); + const response: Send[] = []; + for (const id in sends) { + if (sends.hasOwnProperty(id)) { + response.push(new Send(sends[id])); + } + } + return response; + } + + async getAllDecrypted(): Promise { + if (this.decryptedSendCache != null) { + return this.decryptedSendCache; + } + + const hasKey = await this.cryptoService.hasKey(); + if (!hasKey) { + throw new Error('No key.'); + } + + const decSends: SendView[] = []; + const promises: Promise[] = []; + const sends = await this.getAll(); + sends.forEach((send) => { + promises.push(send.decrypt().then((f) => decSends.push(f))); + }); + + await Promise.all(promises); + decSends.sort(Utils.getSortFunction(this.i18nService, 'name')); + + this.decryptedSendCache = decSends; + return this.decryptedSendCache; + } + + async saveWithServer(sendData: [Send, ArrayBuffer]): Promise { + const request = new SendRequest(sendData[0]); + let response: SendResponse; + if (sendData[0].id == null) { + if (sendData[0].type === SendType.Text) { + response = await this.apiService.postSend(request); + } else { + const fd = new FormData(); + try { + const blob = new Blob([sendData[1]], { type: 'application/octet-stream' }); + fd.append('model', JSON.stringify(request)); + fd.append('data', blob, sendData[0].file.fileName.encryptedString); + } catch (e) { + if (Utils.isNode && !Utils.isBrowser) { + fd.append('model', JSON.stringify(request)); + fd.append('data', Buffer.from(sendData[1]) as any, { + filepath: sendData[0].file.fileName.encryptedString, + contentType: 'application/octet-stream', + } as any); + } else { + throw e; + } + } + response = await this.apiService.postSendFile(fd); + } + sendData[0].id = response.id; + } else { + response = await this.apiService.putSend(sendData[0].id, request); + } + + const userId = await this.userService.getUserId(); + const data = new SendData(response, userId); + await this.upsert(data); + + } + + async upsert(send: SendData | SendData[]): Promise { + const userId = await this.userService.getUserId(); + let sends = await this.storageService.get<{ [id: string]: SendData; }>( + Keys.sendsPrefix + userId); + if (sends == null) { + sends = {}; + } + + if (send instanceof SendData) { + const s = send as SendData; + sends[s.id] = s; + } else { + (send as SendData[]).forEach((s) => { + sends[s.id] = s; + }); + } + + await this.storageService.save(Keys.sendsPrefix + userId, sends); + this.decryptedSendCache = null; + } + + async replace(sends: { [id: string]: SendData; }): Promise { + const userId = await this.userService.getUserId(); + await this.storageService.save(Keys.sendsPrefix + userId, sends); + this.decryptedSendCache = null; + } + + async clear(userId: string): Promise { + await this.storageService.remove(Keys.sendsPrefix + userId); + this.decryptedSendCache = null; + } + + async delete(id: string | string[]): Promise { + const userId = await this.userService.getUserId(); + const sends = await this.storageService.get<{ [id: string]: SendData; }>( + Keys.sendsPrefix + userId); + if (sends == null) { + return; + } + + if (typeof id === 'string') { + if (sends[id] == null) { + return; + } + delete sends[id]; + } else { + (id as string[]).forEach((i) => { + delete sends[i]; + }); + } + + await this.storageService.save(Keys.sendsPrefix + userId, sends); + this.decryptedSendCache = null; + } + + async deleteWithServer(id: string): Promise { + await this.apiService.deleteSend(id); + await this.delete(id); + } + + private parseFile(send: Send, file: File, key: SymmetricCryptoKey): Promise { + return new Promise((resolve, reject) => { + const reader = new FileReader(); + reader.readAsArrayBuffer(file); + reader.onload = async (evt) => { + try { + send.file.fileName = await this.cryptoService.encrypt(file.name, key); + const fileData = await this.cryptoService.encryptToBytes(evt.target.result as ArrayBuffer, key); + resolve(fileData); + } catch (e) { + reject(e); + } + }; + reader.onerror = (evt) => { + reject('Error reading file.'); + }; + }); + } +} diff --git a/src/services/sync.service.ts b/src/services/sync.service.ts index c97968384f..f60df18c90 100644 --- a/src/services/sync.service.ts +++ b/src/services/sync.service.ts @@ -5,6 +5,7 @@ import { CryptoService } from '../abstractions/crypto.service'; import { FolderService } from '../abstractions/folder.service'; import { MessagingService } from '../abstractions/messaging.service'; import { PolicyService } from '../abstractions/policy.service'; +import { SendService } from '../abstractions/send.service'; import { SettingsService } from '../abstractions/settings.service'; import { StorageService } from '../abstractions/storage.service'; import { SyncService as SyncServiceAbstraction } from '../abstractions/sync.service'; @@ -15,6 +16,7 @@ import { CollectionData } from '../models/data/collectionData'; import { FolderData } from '../models/data/folderData'; import { OrganizationData } from '../models/data/organizationData'; import { PolicyData } from '../models/data/policyData'; +import { SendData } from '../models/data/sendData'; import { CipherResponse } from '../models/response/cipherResponse'; import { CollectionDetailsResponse } from '../models/response/collectionResponse'; @@ -26,6 +28,7 @@ import { } from '../models/response/notificationResponse'; import { PolicyResponse } from '../models/response/policyResponse'; import { ProfileResponse } from '../models/response/profileResponse'; +import { SendResponse } from '../models/response/sendResponse'; const Keys = { lastSyncPrefix: 'lastSync_', @@ -39,7 +42,7 @@ export class SyncService implements SyncServiceAbstraction { private cipherService: CipherService, private cryptoService: CryptoService, private collectionService: CollectionService, private storageService: StorageService, private messagingService: MessagingService, private policyService: PolicyService, - private logoutCallback: (expired: boolean) => Promise) { + private sendService: SendService, private logoutCallback: (expired: boolean) => Promise) { } async getLastSync(): Promise { @@ -94,6 +97,7 @@ export class SyncService implements SyncServiceAbstraction { await this.syncFolders(userId, response.folders); await this.syncCollections(response.collections); await this.syncCiphers(userId, response.ciphers); + await this.syncSends(userId, response.sends); await this.syncSettings(userId, response.domains); await this.syncPolicies(response.policies); @@ -287,6 +291,14 @@ export class SyncService implements SyncServiceAbstraction { return await this.cipherService.replace(ciphers); } + private async syncSends(userId: string, response: SendResponse[]) { + const sends: { [id: string]: SendData; } = {}; + response.forEach((s) => { + sends[s.id] = new SendData(s, userId); + }); + return await this.sendService.replace(sends); + } + private async syncSettings(userId: string, response: DomainsResponse) { let eqDomains: string[][] = []; if (response != null && response.equivalentDomains != null) {