diff --git a/src/abstractions/export.service.ts b/src/abstractions/export.service.ts index a6b719e8b3..74b506e954 100644 --- a/src/abstractions/export.service.ts +++ b/src/abstractions/export.service.ts @@ -1,5 +1,5 @@ export abstract class ExportService { - getExport: (format?: 'csv' | 'json') => Promise; - getOrganizationExport: (organizationId: string, format?: 'csv' | 'json') => Promise; + getExport: (format?: 'csv' | 'json' | 'encrypted_json') => Promise; + getOrganizationExport: (organizationId: string, format?: 'csv' | 'json' | 'encrypted_json') => Promise; getFileName: (prefix?: string, extension?: string) => string; } diff --git a/src/angular/components/export.component.ts b/src/angular/components/export.component.ts index 319134d519..38ade297ea 100644 --- a/src/angular/components/export.component.ts +++ b/src/angular/components/export.component.ts @@ -17,13 +17,17 @@ export class ExportComponent { formPromise: Promise; masterPassword: string; - format: 'json' | 'csv' = 'json'; + format: 'json' | 'encrypted_json' | 'csv' = 'json'; showPassword = false; constructor(protected cryptoService: CryptoService, protected i18nService: I18nService, protected platformUtilsService: PlatformUtilsService, protected exportService: ExportService, protected eventService: EventService, protected win: Window) { } + get encryptedFormat() { + return this.format === 'encrypted_json'; + } + async submit() { if (this.masterPassword == null || this.masterPassword === '') { this.platformUtilsService.showToast('error', this.i18nService.t('errorOccurred'), @@ -63,7 +67,16 @@ export class ExportComponent { } protected getFileName(prefix?: string) { - return this.exportService.getFileName(prefix, this.format); + let extension = this.format; + if (this.format === 'encrypted_json') { + if (prefix == null) { + prefix = 'encrypted'; + } else { + prefix = 'encrypted_' + prefix; + } + extension = 'json'; + } + return this.exportService.getFileName(prefix, extension); } protected async collectEvent(): Promise { diff --git a/src/models/export/card.ts b/src/models/export/card.ts index fda37089c9..380f112612 100644 --- a/src/models/export/card.ts +++ b/src/models/export/card.ts @@ -1,5 +1,7 @@ import { CardView } from '../view/cardView'; +import { Card as CardDomain } from '../domain/card'; + export class Card { static template(): Card { const req = new Card(); @@ -29,16 +31,25 @@ export class Card { expYear: string; code: string; - constructor(o?: CardView) { + constructor(o?: CardView | CardDomain) { if (o == null) { return; } - this.cardholderName = o.cardholderName; - this.brand = o.brand; - this.number = o.number; - this.expMonth = o.expMonth; - this.expYear = o.expYear; - this.code = o.code; + if (o instanceof CardView) { + this.cardholderName = o.cardholderName; + this.brand = o.brand; + this.number = o.number; + this.expMonth = o.expMonth; + this.expYear = o.expYear; + this.code = o.code; + } else { + this.cardholderName = o.cardholderName?.encryptedString; + this.brand = o.brand?.encryptedString; + this.number = o.number?.encryptedString; + this.expMonth = o.expMonth?.encryptedString; + this.expYear = o.expYear?.encryptedString; + this.code = o.code?.encryptedString; + } } } diff --git a/src/models/export/cipher.ts b/src/models/export/cipher.ts index 82f9326021..5fb2462f25 100644 --- a/src/models/export/cipher.ts +++ b/src/models/export/cipher.ts @@ -2,6 +2,8 @@ import { CipherType } from '../../enums/cipherType'; import { CipherView } from '../view/cipherView'; +import { Cipher as CipherDomain } from '../domain/cipher'; + import { Card } from './card'; import { Field } from './field'; import { Identity } from './identity'; @@ -70,16 +72,27 @@ export class Cipher { identity: Identity; // Use build method instead of ctor so that we can control order of JSON stringify for pretty print - build(o: CipherView) { + build(o: CipherView | CipherDomain) { this.organizationId = o.organizationId; this.folderId = o.folderId; this.type = o.type; - this.name = o.name; - this.notes = o.notes; + + if (o instanceof CipherView) { + this.name = o.name; + this.notes = o.notes; + } else { + this.name = o.name?.encryptedString; + this.notes = o.notes?.encryptedString; + } + this.favorite = o.favorite; if (o.fields != null) { - this.fields = o.fields.map((f) => new Field(f)); + if (o instanceof CipherView) { + this.fields = o.fields.map((f) => new Field(f)); + } else { + this.fields = o.fields.map((f) => new Field(f)); + } } switch (o.type) { diff --git a/src/models/export/cipherWithIds.ts b/src/models/export/cipherWithIds.ts index a9635b999a..184cd542a2 100644 --- a/src/models/export/cipherWithIds.ts +++ b/src/models/export/cipherWithIds.ts @@ -2,12 +2,14 @@ import { Cipher } from './cipher'; import { CipherView } from '../view/cipherView'; +import { Cipher as CipherDomain } from '../domain/cipher'; + export class CipherWithIds extends Cipher { id: string; collectionIds: string[]; // Use build method instead of ctor so that we can control order of JSON stringify for pretty print - build(o: CipherView) { + build(o: CipherView | CipherDomain) { this.id = o.id; super.build(o); this.collectionIds = o.collectionIds; diff --git a/src/models/export/collection.ts b/src/models/export/collection.ts index d4d9c7c3b1..03b2a42599 100644 --- a/src/models/export/collection.ts +++ b/src/models/export/collection.ts @@ -1,5 +1,7 @@ import { CollectionView } from '../view/collectionView'; +import { Collection as CollectionDomain } from '../domain/collection'; + export class Collection { static template(): Collection { const req = new Collection(); @@ -23,9 +25,13 @@ export class Collection { externalId: string; // Use build method instead of ctor so that we can control order of JSON stringify for pretty print - build(o: CollectionView) { + build(o: CollectionView | CollectionDomain) { this.organizationId = o.organizationId; - this.name = o.name; + if (o instanceof CollectionView) { + this.name = o.name; + } else { + this.name = o.name?.encryptedString; + } this.externalId = o.externalId; } } diff --git a/src/models/export/collectionWithId.ts b/src/models/export/collectionWithId.ts index 10d8181377..ef48ddd941 100644 --- a/src/models/export/collectionWithId.ts +++ b/src/models/export/collectionWithId.ts @@ -2,11 +2,13 @@ import { Collection } from './collection'; import { CollectionView } from '../view/collectionView'; +import { Collection as CollectionDomain } from '../domain/collection'; + export class CollectionWithId extends Collection { id: string; // Use build method instead of ctor so that we can control order of JSON stringify for pretty print - build(o: CollectionView) { + build(o: CollectionView | CollectionDomain) { this.id = o.id; super.build(o); } diff --git a/src/models/export/field.ts b/src/models/export/field.ts index 3e07c65eed..3029b2338e 100644 --- a/src/models/export/field.ts +++ b/src/models/export/field.ts @@ -2,6 +2,8 @@ import { FieldType } from '../../enums/fieldType'; import { FieldView } from '../view/fieldView'; +import { Field as FieldDomain } from '../domain/field'; + export class Field { static template(): Field { const req = new Field(); @@ -22,13 +24,18 @@ export class Field { value: string; type: FieldType; - constructor(o?: FieldView) { + constructor(o?: FieldView | FieldDomain) { if (o == null) { return; } - this.name = o.name; - this.value = o.value; + if (o instanceof FieldView) { + this.name = o.name; + this.value = o.value; + } else { + this.name = o.name?.encryptedString; + this.value = o.value?.encryptedString; + } this.type = o.type; } } diff --git a/src/models/export/folder.ts b/src/models/export/folder.ts index 8c2acf093a..b8d6f43977 100644 --- a/src/models/export/folder.ts +++ b/src/models/export/folder.ts @@ -1,5 +1,7 @@ import { FolderView } from '../view/folderView'; +import { Folder as FolderDomain } from '../domain/folder'; + export class Folder { static template(): Folder { const req = new Folder(); @@ -15,7 +17,11 @@ export class Folder { name: string; // Use build method instead of ctor so that we can control order of JSON stringify for pretty print - build(o: FolderView) { - this.name = o.name; + build(o: FolderView | FolderDomain) { + if (o instanceof FolderView) { + this.name = o.name; + } else { + this.name = o.name?.encryptedString; + } } } diff --git a/src/models/export/folderWithId.ts b/src/models/export/folderWithId.ts index 775376d203..83e57e710f 100644 --- a/src/models/export/folderWithId.ts +++ b/src/models/export/folderWithId.ts @@ -2,11 +2,13 @@ import { Folder } from './folder'; import { FolderView } from '../view/folderView'; +import { Folder as FolderDomain } from '../domain/folder'; + export class FolderWithId extends Folder { id: string; // Use build method instead of ctor so that we can control order of JSON stringify for pretty print - build(o: FolderView) { + build(o: FolderView | FolderDomain) { this.id = o.id; super.build(o); } diff --git a/src/models/export/identity.ts b/src/models/export/identity.ts index a1aae09850..20546ac7f0 100644 --- a/src/models/export/identity.ts +++ b/src/models/export/identity.ts @@ -1,5 +1,7 @@ import { IdentityView } from '../view/identityView'; +import { Identity as IdentityDomain } from '../domain/identity'; + export class Identity { static template(): Identity { const req = new Identity(); @@ -65,28 +67,49 @@ export class Identity { passportNumber: string; licenseNumber: string; - constructor(o?: IdentityView) { + constructor(o?: IdentityView | IdentityDomain) { if (o == null) { return; } - this.title = o.title; - this.firstName = o.firstName; - this.middleName = o.middleName; - this.lastName = o.lastName; - this.address1 = o.address1; - this.address2 = o.address2; - this.address3 = o.address3; - this.city = o.city; - this.state = o.state; - this.postalCode = o.postalCode; - this.country = o.country; - this.company = o.company; - this.email = o.email; - this.phone = o.phone; - this.ssn = o.ssn; - this.username = o.username; - this.passportNumber = o.passportNumber; - this.licenseNumber = o.licenseNumber; + if (o instanceof IdentityView) { + this.title = o.title; + this.firstName = o.firstName; + this.middleName = o.middleName; + this.lastName = o.lastName; + this.address1 = o.address1; + this.address2 = o.address2; + this.address3 = o.address3; + this.city = o.city; + this.state = o.state; + this.postalCode = o.postalCode; + this.country = o.country; + this.company = o.company; + this.email = o.email; + this.phone = o.phone; + this.ssn = o.ssn; + this.username = o.username; + this.passportNumber = o.passportNumber; + this.licenseNumber = o.licenseNumber; + } else { + this.title = o.title?.encryptedString; + this.firstName = o.firstName?.encryptedString; + this.middleName = o.middleName?.encryptedString; + this.lastName = o.lastName?.encryptedString; + this.address1 = o.address1?.encryptedString; + this.address2 = o.address2?.encryptedString; + this.address3 = o.address3?.encryptedString; + this.city = o.city?.encryptedString; + this.state = o.state?.encryptedString; + this.postalCode = o.postalCode?.encryptedString; + this.country = o.country?.encryptedString; + this.company = o.company?.encryptedString; + this.email = o.email?.encryptedString; + this.phone = o.phone?.encryptedString; + this.ssn = o.ssn?.encryptedString; + this.username = o.username?.encryptedString; + this.passportNumber = o.passportNumber?.encryptedString; + this.licenseNumber = o.licenseNumber?.encryptedString; + } } } diff --git a/src/models/export/login.ts b/src/models/export/login.ts index b5401bc939..d8ad6918ab 100644 --- a/src/models/export/login.ts +++ b/src/models/export/login.ts @@ -2,6 +2,8 @@ import { LoginUri } from './loginUri'; import { LoginView } from '../view/loginView'; +import { Login as LoginDomain } from '../domain/login'; + export class Login { static template(): Login { const req = new Login(); @@ -27,17 +29,27 @@ export class Login { password: string; totp: string; - constructor(o?: LoginView) { + constructor(o?: LoginView | LoginDomain) { if (o == null) { return; } if (o.uris != null) { - this.uris = o.uris.map((u) => new LoginUri(u)); + if (o instanceof LoginView) { + this.uris = o.uris.map((u) => new LoginUri(u)); + } else { + this.uris = o.uris.map((u) => new LoginUri(u)); + } } - this.username = o.username; - this.password = o.password; - this.totp = o.totp; + if (o instanceof LoginView) { + this.username = o.username; + this.password = o.password; + this.totp = o.totp; + } else { + this.username = o.username?.encryptedString; + this.password = o.password?.encryptedString; + this.totp = o.totp?.encryptedString; + } } } diff --git a/src/models/export/loginUri.ts b/src/models/export/loginUri.ts index 94c3bbd032..3b416d9fdd 100644 --- a/src/models/export/loginUri.ts +++ b/src/models/export/loginUri.ts @@ -2,6 +2,8 @@ import { UriMatchType } from '../../enums/uriMatchType'; import { LoginUriView } from '../view/loginUriView'; +import { LoginUri as LoginUriDomain } from '../domain/loginUri'; + export class LoginUri { static template(): LoginUri { const req = new LoginUri(); @@ -19,12 +21,16 @@ export class LoginUri { uri: string; match: UriMatchType = null; - constructor(o?: LoginUriView) { + constructor(o?: LoginUriView | LoginUriDomain) { if (o == null) { return; } - this.uri = o.uri; + if (o instanceof LoginUriView) { + this.uri = o.uri; + } else { + this.uri = o.uri?.encryptedString; + } this.match = o.match; } } diff --git a/src/models/export/secureNote.ts b/src/models/export/secureNote.ts index c2115fc337..31a80c04df 100644 --- a/src/models/export/secureNote.ts +++ b/src/models/export/secureNote.ts @@ -2,6 +2,8 @@ import { SecureNoteType } from '../../enums/secureNoteType'; import { SecureNoteView } from '../view/secureNoteView'; +import { SecureNote as SecureNoteDomain } from '../domain/secureNote'; + export class SecureNote { static template(): SecureNote { const req = new SecureNote(); @@ -16,7 +18,7 @@ export class SecureNote { type: SecureNoteType; - constructor(o?: SecureNoteView) { + constructor(o?: SecureNoteView | SecureNoteDomain) { if (o == null) { return; } diff --git a/src/services/export.service.ts b/src/services/export.service.ts index a9b67f617e..03c499d9b1 100644 --- a/src/services/export.service.ts +++ b/src/services/export.service.ts @@ -13,6 +13,7 @@ import { FolderView } from '../models/view/folderView'; import { Cipher } from '../models/domain/cipher'; import { Collection } from '../models/domain/collection'; +import { Folder } from '../models/domain/folder'; import { CipherData } from '../models/data/cipherData'; import { CollectionData } from '../models/data/collectionData'; @@ -26,7 +27,34 @@ export class ExportService implements ExportServiceAbstraction { constructor(private folderService: FolderService, private cipherService: CipherService, private apiService: ApiService) { } - async getExport(format: 'csv' | 'json' = 'csv'): Promise { + async getExport(format: 'csv' | 'json' | 'encrypted_json' = 'csv'): Promise { + if (format === 'encrypted_json') { + return this.getEncryptedExport(); + } else { + return this.getDecryptedExport(format); + } + } + + async getOrganizationExport(organizationId: string, + format: 'csv' | 'json' | 'encrypted_json' = 'csv'): Promise { + if (format === 'encrypted_json') { + return this.getOrganizationEncryptedExport(organizationId); + } else { + return this.getOrganizationDecryptedExport(organizationId, format); + } + } + + getFileName(prefix: string = null, extension: string = 'csv'): 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' + (prefix ? ('_' + prefix) : '') + '_export_' + dateString + '.' + extension; + } + + private async getDecryptedExport(format: 'json' | 'csv'): Promise { let decFolders: FolderView[] = []; let decCiphers: CipherView[] = []; const promises = []; @@ -97,7 +125,38 @@ export class ExportService implements ExportServiceAbstraction { } } - async getOrganizationExport(organizationId: string, format: 'csv' | 'json' = 'csv'): Promise { + private async getEncryptedExport(): Promise { + const folders = await this.folderService.getAll(); + const ciphers = await this.cipherService.getAll(); + + const jsonDoc: any = { + folders: [], + items: [], + }; + + folders.forEach((f) => { + if (f.id == null) { + return; + } + const folder = new FolderExport(); + folder.build(f); + jsonDoc.folders.push(folder); + }); + + ciphers.forEach((c) => { + if (c.organizationId != null) { + return; + } + const cipher = new CipherExport(); + cipher.build(c); + cipher.collectionIds = null; + jsonDoc.items.push(cipher); + }); + + return JSON.stringify(jsonDoc, null, ' '); + } + + private async getOrganizationDecryptedExport(organizationId: string, format: 'json' | 'csv'): Promise { const decCollections: CollectionView[] = []; const decCiphers: CipherView[] = []; const promises = []; @@ -175,14 +234,52 @@ export class ExportService implements ExportServiceAbstraction { } } - getFileName(prefix: string = null, extension: string = 'csv'): 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); + private async getOrganizationEncryptedExport(organizationId: string): Promise { + const collections: Collection[] = []; + const ciphers: Cipher[] = []; + const promises = []; - return 'bitwarden' + (prefix ? ('_' + prefix) : '') + '_export_' + dateString + '.' + extension; + promises.push(this.apiService.getCollections(organizationId).then((c) => { + const collectionPromises: any = []; + if (c != null && c.data != null && c.data.length > 0) { + c.data.forEach((r) => { + const collection = new Collection(new CollectionData(r as CollectionDetailsResponse)); + collections.push(collection); + }); + } + return Promise.all(collectionPromises); + })); + + promises.push(this.apiService.getCiphersOrganization(organizationId).then((c) => { + const cipherPromises: any = []; + if (c != null && c.data != null && c.data.length > 0) { + c.data.forEach((r) => { + const cipher = new Cipher(new CipherData(r)); + ciphers.push(cipher); + }); + } + return Promise.all(cipherPromises); + })); + + await Promise.all(promises); + + const jsonDoc: any = { + collections: [], + items: [], + }; + + collections.forEach((c) => { + const collection = new CollectionExport(); + collection.build(c); + jsonDoc.collections.push(collection); + }); + + ciphers.forEach((c) => { + const cipher = new CipherExport(); + cipher.build(c); + jsonDoc.items.push(cipher); + }); + return JSON.stringify(jsonDoc, null, ' '); } private padNumber(num: number, width: number, padCharacter: string = '0'): string {