diff --git a/package-lock.json b/package-lock.json index 9cab90a199..5f769ef6a9 100644 --- a/package-lock.json +++ b/package-lock.json @@ -151,6 +151,12 @@ "integrity": "sha1-XoS4q/QthOxenQg+jHVzsxVcYzQ=", "dev": true }, + "@types/papaparse": { + "version": "4.1.31", + "resolved": "https://registry.npmjs.org/@types/papaparse/-/papaparse-4.1.31.tgz", + "integrity": "sha512-8+d1hk3GgF+NJ6mMZZ5zKimqIOc+8OTzpLw4RQ8wnS1NkJh/dMH3NEhSud4Ituq2SGXJjOG6wIczCBAKsSsBdQ==", + "dev": true + }, "@types/shelljs": { "version": "0.7.0", "resolved": "https://registry.npmjs.org/@types/shelljs/-/shelljs-0.7.0.tgz", diff --git a/package.json b/package.json index 6ec69a7ab1..b63e1baeb6 100644 --- a/package.json +++ b/package.json @@ -39,6 +39,7 @@ "@angular/upgrade": "5.2.0", "@types/lunr": "2.1.5", "@types/node-forge": "0.7.1", + "@types/papaparse": "4.1.31", "@types/webcrypto": "0.0.28", "angular2-toaster": "4.0.2", "angulartics2": "5.0.1", diff --git a/src/angular/components/export.component.ts b/src/angular/components/export.component.ts new file mode 100644 index 0000000000..f763cf33ab --- /dev/null +++ b/src/angular/components/export.component.ts @@ -0,0 +1,178 @@ +import * as papa from 'papaparse'; + +import { ToasterService } from 'angular2-toaster'; +import { Angulartics2 } from 'angulartics2'; + +import { + EventEmitter, + Output, +} from '@angular/core'; + +import { CipherType } from '../../enums/cipherType'; + +import { CipherView } from '../../models/view/cipherView'; +import { FolderView } from '../../models/view/folderView'; + +import { CipherService } from '../../abstractions/cipher.service'; +import { CryptoService } from '../../abstractions/crypto.service'; +import { FolderService } from '../../abstractions/folder.service'; +import { I18nService } from '../../abstractions/i18n.service'; +import { PlatformUtilsService } from '../../abstractions/platformUtils.service'; +import { UserService } from '../../abstractions/user.service'; + +export class ExportComponent { + @Output() onSaved = new EventEmitter(); + + masterPassword: string; + showPassword = false; + + constructor(protected analytics: Angulartics2, protected toasterService: ToasterService, + protected cipherService: CipherService, protected folderService: FolderService, + protected cryptoService: CryptoService, protected userService: UserService, + protected i18nService: I18nService, protected platformUtilsService: PlatformUtilsService, + protected win: Window) { } + + async submit() { + if (this.masterPassword == null || this.masterPassword === '') { + this.toasterService.popAsync('error', this.i18nService.t('errorOccurred'), + this.i18nService.t('invalidMasterPassword')); + return; + } + + const email = await this.userService.getEmail(); + const key = this.cryptoService.makeKey(this.masterPassword, email); + const keyHash = await this.cryptoService.hashPassword(this.masterPassword, key); + const storedKeyHash = await this.cryptoService.getKeyHash(); + + if (storedKeyHash != null && keyHash != null && storedKeyHash === keyHash) { + const csv = await this.getCsv(); + this.analytics.eventTrack.next({ action: 'Exported Data' }); + this.downloadFile(csv); + this.saved(); + } else { + this.toasterService.popAsync('error', this.i18nService.t('errorOccurred'), + this.i18nService.t('invalidMasterPassword')); + } + } + + togglePassword() { + this.analytics.eventTrack.next({ action: 'Toggled Master Password on Export' }); + this.showPassword = !this.showPassword; + document.getElementById('masterPassword').focus(); + } + + protected saved() { + this.onSaved.emit(); + } + + private async checkPassword() { + const email = await this.userService.getEmail(); + const key = this.cryptoService.makeKey(this.masterPassword, email); + const keyHash = await this.cryptoService.hashPassword(this.masterPassword, key); + const storedKeyHash = await this.cryptoService.getKeyHash(); + if (storedKeyHash == null || keyHash == null || storedKeyHash !== keyHash) { + throw new Error('Invalid password.'); + } + } + + private async getCsv(): Promise { + let decFolders: FolderView[] = []; + let decCiphers: CipherView[] = []; + const promises = []; + + promises.push(this.folderService.getAllDecrypted().then((folders) => { + decFolders = folders; + })); + + promises.push(this.cipherService.getAllDecrypted().then((ciphers) => { + decCiphers = ciphers; + })); + + await Promise.all(promises); + + const foldersMap = new Map(); + decFolders.forEach((f) => { + foldersMap.set(f.id, f); + }); + + const exportCiphers: any[] = []; + decCiphers.forEach((c) => { + // only export logins and secure notes + if (c.type !== CipherType.Login && c.type !== CipherType.SecureNote) { + return; + } + + const cipher: any = { + folder: c.folderId && foldersMap.has(c.folderId) ? foldersMap.get(c.folderId).name : null, + favorite: c.favorite ? 1 : null, + type: null, + name: c.name, + notes: c.notes, + fields: null, + // Login props + login_uri: null, + login_username: null, + login_password: null, + login_totp: null, + }; + + if (c.fields) { + c.fields.forEach((f: any) => { + if (!cipher.fields) { + cipher.fields = ''; + } else { + cipher.fields += '\n'; + } + + cipher.fields += ((f.name || '') + ': ' + f.value); + }); + } + + switch (c.type) { + case CipherType.Login: + cipher.type = 'login'; + cipher.login_username = c.login.username; + cipher.login_password = c.login.password; + cipher.login_totp = c.login.totp; + + if (c.login.uris) { + cipher.login_uri = []; + c.login.uris.forEach((u) => { + cipher.login_uri.push(u.uri); + }); + } + break; + case CipherType.SecureNote: + cipher.type = 'note'; + break; + default: + return; + } + + exportCiphers.push(cipher); + }); + + return papa.unparse(exportCiphers); + } + + private downloadFile(csv: string): void { + const fileName = this.makeFileName(); + this.platformUtilsService.saveFile(this.win, csv, { type: 'text/plain' }, fileName); + } + + private makeFileName(): string { + const now = new Date(); + const dateString = + now.getFullYear() + '' + this.padNumber(now.getMonth() + 1, 2) + '' + this.padNumber(now.getDate(), 2) + + this.padNumber(now.getHours(), 2) + '' + this.padNumber(now.getMinutes(), 2) + + this.padNumber(now.getSeconds(), 2); + + return 'bitwarden_export_' + dateString + '.csv'; + } + + private padNumber(num: number, width: number, padCharacter: string = '0'): string { + const numString = num.toString(); + return numString.length >= width ? numString : + new Array(width - numString.length + 1).join(padCharacter) + numString; + } +}