import { DatePipe } from '@angular/common'; import { EventEmitter, Input, Output, } from '@angular/core'; import { Component } from '@angular/core'; import { SendType } from 'jslib/enums/sendType'; import { ApiService } from 'jslib/abstractions/api.service'; import { CryptoService } from 'jslib/abstractions/crypto.service'; import { CryptoFunctionService } from 'jslib/abstractions/cryptoFunction.service'; import { EnvironmentService } from 'jslib/abstractions/environment.service'; import { I18nService } from 'jslib/abstractions/i18n.service'; import { PlatformUtilsService } from 'jslib/abstractions/platformUtils.service'; import { SendView } from 'jslib/models/view/sendView'; import { SendFileView } from 'jslib/models/view/sendFileView'; import { SendTextView } from 'jslib/models/view/sendTextView'; import { Send } from 'jslib/models/domain/send'; import { SendFile } from 'jslib/models/domain/sendFile'; import { SendText } from 'jslib/models/domain/sendText'; import { SendData } from 'jslib/models/data/sendData'; import { SendRequest } from 'jslib/models/request/sendRequest'; import { Utils } from 'jslib/misc/utils'; @Component({ selector: 'app-send-add-edit', templateUrl: 'add-edit.component.html', }) export class AddEditComponent { @Input() sendId: string; @Input() type: SendType; @Output() onSavedSend = new EventEmitter(); @Output() onDeletedSend = new EventEmitter(); @Output() onCancelled = new EventEmitter(); editMode: boolean = false; send: SendView; link: string; title: string; deletionDate: string; expirationDate: string; hasPassword: boolean; password: string; formPromise: Promise; deletePromise: Promise; sendType = SendType; typeOptions: any[]; constructor(private i18nService: I18nService, private platformUtilsService: PlatformUtilsService, private apiService: ApiService, private cryptoService: CryptoService, private cryptoFunctionService: CryptoFunctionService, private environmentService: EnvironmentService, private datePipe: DatePipe) { this.typeOptions = [ { name: i18nService.t('sendTypeFile'), value: SendType.File }, { name: i18nService.t('sendTypeText'), value: SendType.Text }, ]; } async ngOnInit() { await this.load(); } async load() { this.editMode = this.sendId != null; if (this.editMode) { this.editMode = true; this.title = this.i18nService.t('editSend'); } else { this.title = this.i18nService.t('createSend'); } if (this.send == null) { if (this.editMode) { const send = await this.loadSend(); this.send = await send.decrypt(); } else { this.send = new SendView(); this.send.type = this.type == null ? SendType.File : this.type; this.send.file = new SendFileView(); this.send.text = new SendTextView(); this.send.deletionDate = new Date(); this.send.deletionDate.setDate(this.send.deletionDate.getDate() + 7); } } this.hasPassword = this.send.password != null && this.send.password.trim() !== ''; // Parse dates this.deletionDate = this.send.deletionDate == null ? null : this.datePipe.transform(this.send.deletionDate, 'yyyy-MM-ddTHH:mm'); this.expirationDate = this.send.expirationDate == null ? null : this.datePipe.transform(this.send.expirationDate, 'yyyy-MM-ddTHH:mm'); if (this.editMode) { let webVaultUrl = this.environmentService.getWebVaultUrl(); if (webVaultUrl == null) { webVaultUrl = 'https://vault.bitwarden.com'; } this.link = webVaultUrl + '/#/send/' + this.send.accessId + '/' + this.send.urlB64Key; } } async submit(): Promise { if (this.send.name == null || this.send.name === '') { this.platformUtilsService.showToast('error', this.i18nService.t('errorOccurred'), this.i18nService.t('nameRequired')); return false; } let file: File = null; if (this.send.type === SendType.File && !this.editMode) { const fileEl = document.getElementById('file') as HTMLInputElement; const files = fileEl.files; if (files == null || files.length === 0) { this.platformUtilsService.showToast('error', this.i18nService.t('errorOccurred'), this.i18nService.t('selectFile')); return; } file = files[0]; if (file.size > 104857600) { // 100 MB this.platformUtilsService.showToast('error', this.i18nService.t('errorOccurred'), this.i18nService.t('maxFileSize')); return; } } const encSend = await this.encryptSend(file); try { this.formPromise = this.saveSend(encSend); await this.formPromise; this.send.id = encSend[0].id; this.platformUtilsService.showToast('success', null, this.i18nService.t(this.editMode ? 'editedSend' : 'createdSend')); this.onSavedSend.emit(this.send); return true; } catch { } return false; } clearExpiration() { this.expirationDate = null; } async delete(): Promise { if (this.deletePromise != null) { return; } const confirmed = await this.platformUtilsService.showDialog( this.i18nService.t('deleteSendConfirmation'), this.i18nService.t('deleteSend'), this.i18nService.t('yes'), this.i18nService.t('no'), 'warning'); if (!confirmed) { return; } try { this.deletePromise = this.apiService.deleteSend(this.send.id); await this.deletePromise; this.platformUtilsService.showToast('success', null, this.i18nService.t('deletedSend')); await this.load(); this.onDeletedSend.emit(this.send); } catch { } } protected async loadSend(): Promise { const response = await this.apiService.getSend(this.sendId); const data = new SendData(response); return new Send(data); } protected async encryptSend(file: File): Promise<[Send, ArrayBuffer]> { let fileData: ArrayBuffer = null; const send = new Send(); send.id = this.send.id; send.type = this.send.type; send.disabled = this.send.disabled; send.maxAccessCount = this.send.maxAccessCount; if (this.send.key == null) { this.send.key = await this.cryptoFunctionService.randomBytes(16); this.send.cryptoKey = await this.cryptoService.makeSendKey(this.send.key); } if (this.password != null) { const passwordHash = await this.cryptoFunctionService.pbkdf2(this.password, this.send.key, 'sha256', 100000); send.password = Utils.fromBufferToB64(passwordHash); } send.key = await this.cryptoService.encrypt(this.send.key, null); send.name = await this.cryptoService.encrypt(this.send.name, this.send.cryptoKey); send.notes = await this.cryptoService.encrypt(this.send.notes, this.send.cryptoKey); if (send.type === SendType.Text) { send.text = new SendText(); send.text.text = await this.cryptoService.encrypt(this.send.text.text, this.send.cryptoKey); send.text.hidden = this.send.text.hidden; } else if (send.type === SendType.File) { send.file = new SendFile(); if (file != null) { fileData = await this.parseFile(send, file); } } // Parse dates try { send.deletionDate = this.deletionDate == null ? null : new Date(this.deletionDate); } catch { send.deletionDate = null; } try { send.expirationDate = this.expirationDate == null ? null : new Date(this.expirationDate); } catch { send.expirationDate = null; } return [send, fileData]; } protected async saveSend(sendData: [Send, ArrayBuffer]) { const request = new SendRequest(sendData[0]); if (sendData[0].id == null) { if (sendData[0].type === SendType.Text) { 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; } } await this.apiService.postSendFile(fd); } } else { await this.apiService.putSend(sendData[0].id, request); } } private parseFile(send: Send, file: File): 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, this.send.cryptoKey); const fileData = await this.cryptoService.encryptToBytes(evt.target.result as ArrayBuffer, this.send.cryptoKey); resolve(fileData); } catch (e) { reject(e); } }; reader.onerror = (evt) => { reject('Error reading file.'); }; }); } }