diff --git a/common/src/abstractions/export.service.ts b/common/src/abstractions/export.service.ts index fa0b63ff06..1d3fbce758 100644 --- a/common/src/abstractions/export.service.ts +++ b/common/src/abstractions/export.service.ts @@ -2,6 +2,11 @@ import { EventView } from "../models/view/eventView"; export abstract class ExportService { getExport: (format?: "csv" | "json" | "encrypted_json") => Promise; + getPasswordProtectedExport: ( + password: string, + format?: "csv" | "json" | "encrypted_json", + organizationId?: string + ) => Promise; getOrganizationExport: ( organizationId: string, format?: "csv" | "json" | "encrypted_json" diff --git a/common/src/abstractions/import.service.ts b/common/src/abstractions/import.service.ts index e9c92a189f..799ddd92a2 100644 --- a/common/src/abstractions/import.service.ts +++ b/common/src/abstractions/import.service.ts @@ -1,13 +1,14 @@ import { Importer } from "../importers/importer"; +import { ImportType } from "../services/import.service"; export interface ImportOption { id: string; name: string; } export abstract class ImportService { - featuredImportOptions: ImportOption[]; - regularImportOptions: ImportOption[]; + featuredImportOptions: readonly ImportOption[]; + regularImportOptions: readonly ImportOption[]; getImportOptions: () => ImportOption[]; import: (importer: Importer, fileContents: string, organizationId?: string) => Promise; - getImporter: (format: string, organizationId: string) => Importer; + getImporter: (format: ImportType, organizationId: string, password?: string) => Importer; } diff --git a/common/src/importers/bitwardenPasswordProtectedImporter.ts b/common/src/importers/bitwardenPasswordProtectedImporter.ts new file mode 100644 index 0000000000..1e19321282 --- /dev/null +++ b/common/src/importers/bitwardenPasswordProtectedImporter.ts @@ -0,0 +1,101 @@ +import { BaseImporter } from "./baseImporter"; +import { Importer } from "./importer"; + +import { EncString } from "../models/domain/encString"; +import { ImportResult } from "../models/domain/importResult"; + +import { CryptoService } from "../abstractions/crypto.service"; +import { I18nService } from "../abstractions/i18n.service"; +import { ImportService } from "../abstractions/import.service"; +import { KdfType } from "../enums/kdfType"; +import { SymmetricCryptoKey } from "../models/domain/symmetricCryptoKey"; + +class BitwardenPasswordProtectedFileFormat { + encrypted: boolean; + passwordProtected: boolean; + format: "json" | "csv" | "encrypted_json"; + salt: string; + kdfIterations: number; + kdfType: number; + // tslint:disable-next-line + encKeyValidation_DO_NOT_EDIT: string; + data: string; +} + +export class BitwardenPasswordProtectedImporter extends BaseImporter implements Importer { + private innerImporter: Importer; + private key: SymmetricCryptoKey; + + constructor( + private importService: ImportService, + private cryptoService: CryptoService, + private i18nService: I18nService, + private password: string + ) { + super(); + } + + async parse(data: string): Promise { + const result = new ImportResult(); + const parsedData = JSON.parse(data); + if (this.cannotParseFile(parsedData)) { + result.success = false; + return result; + } + + this.setInnerImporter(parsedData.format); + + if (!(await this.checkPassword(parsedData))) { + result.success = false; + result.errorMessage = this.i18nService.t("importEncKeyError"); + return result; + } + + const encData = new EncString(parsedData.data); + const clearTextData = await this.cryptoService.decryptToUtf8(encData, this.key); + return this.innerImporter.parse(clearTextData); + } + + private async checkPassword(jdoc: BitwardenPasswordProtectedFileFormat): Promise { + this.key = await this.cryptoService.makePinKey( + this.password, + jdoc.salt, + KdfType.PBKDF2_SHA256, + jdoc.kdfIterations + ); + + const encKeyValidation = new EncString(jdoc.encKeyValidation_DO_NOT_EDIT); + + const encKeyValidationDecrypt = await this.cryptoService.decryptToUtf8( + encKeyValidation, + this.key + ); + if (encKeyValidationDecrypt === null) { + return false; + } + return true; + } + + private cannotParseFile(jdoc: BitwardenPasswordProtectedFileFormat): boolean { + return ( + !jdoc || + !jdoc.encrypted || + !jdoc.passwordProtected || + !(jdoc.format === "csv" || jdoc.format === "json" || jdoc.format === "encrypted_json") || + !jdoc.salt || + !jdoc.kdfIterations || + typeof jdoc.kdfIterations !== "number" || + jdoc.kdfType == null || + KdfType[jdoc.kdfType] == null || + !jdoc.encKeyValidation_DO_NOT_EDIT || + !jdoc.data + ); + } + + private setInnerImporter(format: "csv" | "json" | "encrypted_json") { + this.innerImporter = + format === "csv" + ? this.importService.getImporter("bitwardencsv", this.organizationId) + : this.importService.getImporter("bitwardenjson", this.organizationId); + } +} diff --git a/common/src/services/export.service.ts b/common/src/services/export.service.ts index 31b1b0b6f9..cbc18fe42b 100644 --- a/common/src/services/export.service.ts +++ b/common/src/services/export.service.ts @@ -1,10 +1,12 @@ import * as papa from "papaparse"; import { CipherType } from "../enums/cipherType"; +import { KdfType } from "../enums/kdfType"; import { ApiService } from "../abstractions/api.service"; import { CipherService } from "../abstractions/cipher.service"; import { CryptoService } from "../abstractions/crypto.service"; +import { CryptoFunctionService } from "../abstractions/cryptoFunction.service"; import { ExportService as ExportServiceAbstraction } from "../abstractions/export.service"; import { FolderService } from "../abstractions/folder.service"; @@ -33,7 +35,8 @@ export class ExportService implements ExportServiceAbstraction { private folderService: FolderService, private cipherService: CipherService, private apiService: ApiService, - private cryptoService: CryptoService + private cryptoService: CryptoService, + private cryptoFunctionService: CryptoFunctionService ) {} async getExport(format: "csv" | "json" | "encrypted_json" = "csv"): Promise { @@ -44,6 +47,41 @@ export class ExportService implements ExportServiceAbstraction { } } + async getPasswordProtectedExport( + password: string, + format: "csv" | "json" | "encrypted_json" = "csv", + organizationId?: string + ): Promise { + const clearText = organizationId + ? await this.getOrganizationExport(organizationId, format) + : await this.getExport(format); + + const salt = Utils.fromBufferToB64(await this.cryptoFunctionService.randomBytes(16)); + const kdfIterations = 100000; + const key = await this.cryptoService.makePinKey( + password, + salt, + KdfType.PBKDF2_SHA256, + kdfIterations + ); + + const encKeyValidation = await this.cryptoService.encrypt(Utils.newGuid(), key); + const encText = await this.cryptoService.encrypt(clearText, key); + + const jsonDoc: any = { + encrypted: true, + passwordProtected: true, + format: format, + salt: salt, + kdfIterations: kdfIterations, + kdfType: KdfType.PBKDF2_SHA256, + encKeyValidation_DO_NOT_EDIT: encKeyValidation.encryptedString, + data: encText.encryptedString, + }; + + return JSON.stringify(jsonDoc, null, " "); + } + async getOrganizationExport( organizationId: string, format: "csv" | "json" | "encrypted_json" = "csv" diff --git a/common/src/services/import.service.ts b/common/src/services/import.service.ts index 825d59042b..332e581533 100644 --- a/common/src/services/import.service.ts +++ b/common/src/services/import.service.ts @@ -32,6 +32,7 @@ import { AvastJsonImporter } from "../importers/avastJsonImporter"; import { AviraCsvImporter } from "../importers/aviraCsvImporter"; import { BitwardenCsvImporter } from "../importers/bitwardenCsvImporter"; import { BitwardenJsonImporter } from "../importers/bitwardenJsonImporter"; +import { BitwardenPasswordProtectedImporter } from "../importers/bitwardenPasswordProtectedImporter"; import { BlackBerryCsvImporter } from "../importers/blackBerryCsvImporter"; import { BlurCsvImporter } from "../importers/blurCsvImporter"; import { ButtercupCsvImporter } from "../importers/buttercupCsvImporter"; @@ -82,70 +83,79 @@ import { UpmCsvImporter } from "../importers/upmCsvImporter"; import { YotiCsvImporter } from "../importers/yotiCsvImporter"; import { ZohoVaultCsvImporter } from "../importers/zohoVaultCsvImporter"; -export class ImportService implements ImportServiceAbstraction { - featuredImportOptions = [ - { id: "bitwardenjson", name: "Bitwarden (json)" }, - { id: "bitwardencsv", name: "Bitwarden (csv)" }, - { id: "chromecsv", name: "Chrome (csv)" }, - { id: "dashlanejson", name: "Dashlane (json)" }, - { id: "firefoxcsv", name: "Firefox (csv)" }, - { id: "keepass2xml", name: "KeePass 2 (xml)" }, - { id: "lastpasscsv", name: "LastPass (csv)" }, - { id: "safaricsv", name: "Safari and macOS (csv)" }, - { id: "1password1pif", name: "1Password (1pif)" }, - ]; +const featuredImportOptions = [ + { id: "bitwardenjson", name: "Bitwarden (json)" }, + { id: "bitwardencsv", name: "Bitwarden (csv)" }, + { id: "chromecsv", name: "Chrome (csv)" }, + { id: "dashlanejson", name: "Dashlane (json)" }, + { id: "firefoxcsv", name: "Firefox (csv)" }, + { id: "keepass2xml", name: "KeePass 2 (xml)" }, + { id: "lastpasscsv", name: "LastPass (csv)" }, + { id: "safaricsv", name: "Safari and macOS (csv)" }, + { id: "1password1pif", name: "1Password (1pif)" }, +] as const; - regularImportOptions: ImportOption[] = [ - { id: "keepassxcsv", name: "KeePassX (csv)" }, - { id: "1passwordwincsv", name: "1Password 6 and 7 Windows (csv)" }, - { id: "1passwordmaccsv", name: "1Password 6 and 7 Mac (csv)" }, - { id: "roboformcsv", name: "RoboForm (csv)" }, - { id: "keepercsv", name: "Keeper (csv)" }, - // Temporarily remove this option for the Feb release - // { id: "keeperjson", name: "Keeper (json)" }, - { id: "enpasscsv", name: "Enpass (csv)" }, - { id: "enpassjson", name: "Enpass (json)" }, - { id: "safeincloudxml", name: "SafeInCloud (xml)" }, - { id: "pwsafexml", name: "Password Safe (xml)" }, - { id: "stickypasswordxml", name: "Sticky Password (xml)" }, - { id: "msecurecsv", name: "mSecure (csv)" }, - { id: "truekeycsv", name: "True Key (csv)" }, - { id: "passwordbossjson", name: "Password Boss (json)" }, - { id: "zohovaultcsv", name: "Zoho Vault (csv)" }, - { id: "splashidcsv", name: "SplashID (csv)" }, - { id: "passworddragonxml", name: "Password Dragon (xml)" }, - { id: "padlockcsv", name: "Padlock (csv)" }, - { id: "passboltcsv", name: "Passbolt (csv)" }, - { id: "clipperzhtml", name: "Clipperz (html)" }, - { id: "aviracsv", name: "Avira (csv)" }, - { id: "saferpasscsv", name: "SaferPass (csv)" }, - { id: "upmcsv", name: "Universal Password Manager (csv)" }, - { id: "ascendocsv", name: "Ascendo DataVault (csv)" }, - { id: "meldiumcsv", name: "Meldium (csv)" }, - { id: "passkeepcsv", name: "PassKeep (csv)" }, - { id: "operacsv", name: "Opera (csv)" }, - { id: "vivaldicsv", name: "Vivaldi (csv)" }, - { id: "gnomejson", name: "GNOME Passwords and Keys/Seahorse (json)" }, - { id: "blurcsv", name: "Blur (csv)" }, - { id: "passwordagentcsv", name: "Password Agent (csv)" }, - { id: "passpackcsv", name: "Passpack (csv)" }, - { id: "passmanjson", name: "Passman (json)" }, - { id: "avastcsv", name: "Avast Passwords (csv)" }, - { id: "avastjson", name: "Avast Passwords (json)" }, - { id: "fsecurefsk", name: "F-Secure KEY (fsk)" }, - { id: "kasperskytxt", name: "Kaspersky Password Manager (txt)" }, - { id: "remembearcsv", name: "RememBear (csv)" }, - { id: "passwordwallettxt", name: "PasswordWallet (txt)" }, - { id: "mykicsv", name: "Myki (csv)" }, - { id: "securesafecsv", name: "SecureSafe (csv)" }, - { id: "logmeoncecsv", name: "LogMeOnce (csv)" }, - { id: "blackberrycsv", name: "BlackBerry Password Keeper (csv)" }, - { id: "buttercupcsv", name: "Buttercup (csv)" }, - { id: "codebookcsv", name: "Codebook (csv)" }, - { id: "encryptrcsv", name: "Encryptr (csv)" }, - { id: "yoticsv", name: "Yoti (csv)" }, - { id: "nordpasscsv", name: "Nordpass (csv)" }, - ]; +const regularImportOptions = [ + { id: "keepassxcsv", name: "KeePassX (csv)" }, + { id: "1passwordwincsv", name: "1Password 6 and 7 Windows (csv)" }, + { id: "1passwordmaccsv", name: "1Password 6 and 7 Mac (csv)" }, + { id: "roboformcsv", name: "RoboForm (csv)" }, + { id: "keepercsv", name: "Keeper (csv)" }, + // Temporarily remove this option for the Feb release + // { id: "keeperjson", name: "Keeper (json)" }, + { id: "enpasscsv", name: "Enpass (csv)" }, + { id: "enpassjson", name: "Enpass (json)" }, + { id: "safeincloudxml", name: "SafeInCloud (xml)" }, + { id: "pwsafexml", name: "Password Safe (xml)" }, + { id: "stickypasswordxml", name: "Sticky Password (xml)" }, + { id: "msecurecsv", name: "mSecure (csv)" }, + { id: "truekeycsv", name: "True Key (csv)" }, + { id: "passwordbossjson", name: "Password Boss (json)" }, + { id: "zohovaultcsv", name: "Zoho Vault (csv)" }, + { id: "splashidcsv", name: "SplashID (csv)" }, + { id: "passworddragonxml", name: "Password Dragon (xml)" }, + { id: "padlockcsv", name: "Padlock (csv)" }, + { id: "passboltcsv", name: "Passbolt (csv)" }, + { id: "clipperzhtml", name: "Clipperz (html)" }, + { id: "aviracsv", name: "Avira (csv)" }, + { id: "saferpasscsv", name: "SaferPass (csv)" }, + { id: "upmcsv", name: "Universal Password Manager (csv)" }, + { id: "ascendocsv", name: "Ascendo DataVault (csv)" }, + { id: "meldiumcsv", name: "Meldium (csv)" }, + { id: "passkeepcsv", name: "PassKeep (csv)" }, + { id: "operacsv", name: "Opera (csv)" }, + { id: "vivaldicsv", name: "Vivaldi (csv)" }, + { id: "gnomejson", name: "GNOME Passwords and Keys/Seahorse (json)" }, + { id: "blurcsv", name: "Blur (csv)" }, + { id: "passwordagentcsv", name: "Password Agent (csv)" }, + { id: "passpackcsv", name: "Passpack (csv)" }, + { id: "passmanjson", name: "Passman (json)" }, + { id: "avastcsv", name: "Avast Passwords (csv)" }, + { id: "avastjson", name: "Avast Passwords (json)" }, + { id: "fsecurefsk", name: "F-Secure KEY (fsk)" }, + { id: "kasperskytxt", name: "Kaspersky Password Manager (txt)" }, + { id: "remembearcsv", name: "RememBear (csv)" }, + { id: "passwordwallettxt", name: "PasswordWallet (txt)" }, + { id: "mykicsv", name: "Myki (csv)" }, + { id: "securesafecsv", name: "SecureSafe (csv)" }, + { id: "logmeoncecsv", name: "LogMeOnce (csv)" }, + { id: "blackberrycsv", name: "BlackBerry Password Keeper (csv)" }, + { id: "buttercupcsv", name: "Buttercup (csv)" }, + { id: "codebookcsv", name: "Codebook (csv)" }, + { id: "encryptrcsv", name: "Encryptr (csv)" }, + { id: "yoticsv", name: "Yoti (csv)" }, + { id: "nordpasscsv", name: "Nordpass (csv)" }, +] as const; + +export type ImportType = + | typeof featuredImportOptions[number]["id"] + | typeof regularImportOptions[number]["id"] + | "bitwardenpasswordprotected"; + +export class ImportService implements ImportServiceAbstraction { + featuredImportOptions = featuredImportOptions as readonly ImportOption[]; + + regularImportOptions = regularImportOptions as readonly ImportOption[]; constructor( private cipherService: CipherService, @@ -198,8 +208,12 @@ export class ImportService implements ImportServiceAbstraction { } } - getImporter(format: string, organizationId: string = null): Importer { - const importer = this.getImporterInstance(format); + getImporter( + format: ImportType, + organizationId: string = null, + password: string = null + ): Importer { + const importer = this.getImporterInstance(format, password); if (importer == null) { return null; } @@ -207,8 +221,8 @@ export class ImportService implements ImportServiceAbstraction { return importer; } - private getImporterInstance(format: string) { - if (format == null || format === "") { + private getImporterInstance(format: ImportType, password: string) { + if (format == null) { return null; } @@ -217,6 +231,13 @@ export class ImportService implements ImportServiceAbstraction { return new BitwardenCsvImporter(); case "bitwardenjson": return new BitwardenJsonImporter(this.cryptoService, this.i18nService); + case "bitwardenpasswordprotected": + return new BitwardenPasswordProtectedImporter( + this, + this.cryptoService, + this.i18nService, + password + ); case "lastpasscsv": case "passboltcsv": return new LastPassCsvImporter(); @@ -254,8 +275,8 @@ export class ImportService implements ImportServiceAbstraction { return new OnePasswordMacCsvImporter(); case "keepercsv": return new KeeperCsvImporter(); - case "keeperjson": - return new KeeperJsonImporter(); + // case "keeperjson": + // return new KeeperJsonImporter(); case "passworddragonxml": return new PasswordDragonXmlImporter(); case "enpasscsv": diff --git a/spec/common/importers/bitwardenPasswordProtectedImporter.spec.ts b/spec/common/importers/bitwardenPasswordProtectedImporter.spec.ts new file mode 100644 index 0000000000..77e16bc38f --- /dev/null +++ b/spec/common/importers/bitwardenPasswordProtectedImporter.spec.ts @@ -0,0 +1,203 @@ +import Substitute, { Arg, SubstituteOf } from "@fluffy-spoon/substitute"; + +import { CryptoService } from "jslib-common/abstractions/crypto.service"; +import { I18nService } from "jslib-common/abstractions/i18n.service"; +import { ImportService } from "jslib-common/abstractions/import.service"; + +import { KdfType } from "jslib-common/enums/kdfType"; + +import { BitwardenPasswordProtectedImporter } from "jslib-common/importers/bitwardenPasswordProtectedImporter"; +import { Importer } from "jslib-common/importers/importer"; + +import { Utils } from "jslib-common/misc/utils"; +import { ImportResult } from "jslib-common/models/domain/importResult"; + +describe("BitwardenPasswordProtectedImporter", () => { + let importer: BitwardenPasswordProtectedImporter; + let innerImporter: SubstituteOf; + let importService: SubstituteOf; + let cryptoService: SubstituteOf; + let i18nService: SubstituteOf; + const password = Utils.newGuid(); + const result = new ImportResult(); + let jDoc: { + encrypted?: boolean; + passwordProtected?: boolean; + format?: string; + salt?: string; + kdfIterations?: any; + kdfType?: any; + encKeyValidation_DO_NOT_EDIT?: string; + data?: string; + }; + + beforeEach(() => { + cryptoService = Substitute.for(); + i18nService = Substitute.for(); + importService = Substitute.for(); + innerImporter = Substitute.for(); + + jDoc = { + encrypted: true, + passwordProtected: true, + format: "csv", + salt: "c2FsdA==", + kdfIterations: 100000, + kdfType: KdfType.PBKDF2_SHA256, + encKeyValidation_DO_NOT_EDIT: Utils.newGuid(), + data: Utils.newGuid(), + }; + + result.success = true; + innerImporter.parse(Arg.any()).resolves(result); + importer = new BitwardenPasswordProtectedImporter( + importService, + cryptoService, + i18nService, + password + ); + }); + + describe("Required Json Data", () => { + it("succeeds with default jdoc", async () => { + cryptoService.decryptToUtf8(Arg.any(), Arg.any()).resolves("successful decryption"); + + expect((await importer.parse(JSON.stringify(jDoc))).success).toEqual(true); + }); + + it("accepts json format", async () => { + jDoc.format = "json"; + cryptoService.decryptToUtf8(Arg.any(), Arg.any()).resolves("successful decryption"); + + expect((await importer.parse(JSON.stringify(jDoc))).success).toEqual(true); + }); + + it("accepts encrypted_json format", async () => { + jDoc.format = "encrypted_json"; + cryptoService.decryptToUtf8(Arg.any(), Arg.any()).resolves("successful decryption"); + + expect((await importer.parse(JSON.stringify(jDoc))).success).toEqual(true); + }); + + it("fails if encrypted === false", async () => { + jDoc.encrypted = false; + expect((await importer.parse(JSON.stringify(jDoc))).success).toEqual(false); + }); + + it("fails if encrypted === null", async () => { + jDoc.encrypted = null; + expect((await importer.parse(JSON.stringify(jDoc))).success).toEqual(false); + }); + + it("fails if passwordProtected === false", async () => { + jDoc.passwordProtected = false; + expect((await importer.parse(JSON.stringify(jDoc))).success).toEqual(false); + }); + + it("fails if passwordProtected === null", async () => { + jDoc.passwordProtected = null; + expect((await importer.parse(JSON.stringify(jDoc))).success).toEqual(false); + }); + + it("fails if format === null", async () => { + jDoc.format = null; + expect((await importer.parse(JSON.stringify(jDoc))).success).toEqual(false); + }); + + it("fails if format not known", async () => { + jDoc.format = "Not a real format"; + expect((await importer.parse(JSON.stringify(jDoc))).success).toEqual(false); + }); + + it("fails if salt === null", async () => { + jDoc.salt = null; + expect((await importer.parse(JSON.stringify(jDoc))).success).toEqual(false); + }); + + it("fails if kdfIterations === null", async () => { + jDoc.kdfIterations = null; + expect((await importer.parse(JSON.stringify(jDoc))).success).toEqual(false); + }); + + it("fails if kdfIterations is not a number", async () => { + jDoc.kdfIterations = "not a number"; + expect((await importer.parse(JSON.stringify(jDoc))).success).toEqual(false); + }); + + it("fails if kdfType === null", async () => { + jDoc.kdfType = null; + expect((await importer.parse(JSON.stringify(jDoc))).success).toEqual(false); + }); + + it("fails if kdfType is not a string", async () => { + jDoc.kdfType = "not a valid kdf type"; + expect((await importer.parse(JSON.stringify(jDoc))).success).toEqual(false); + }); + + it("fails if kdfType is not a known kdfType", async () => { + jDoc.kdfType = -1; + expect((await importer.parse(JSON.stringify(jDoc))).success).toEqual(false); + }); + + it("fails if encKeyValidation_DO_NOT_EDIT === null", async () => { + jDoc.encKeyValidation_DO_NOT_EDIT = null; + expect((await importer.parse(JSON.stringify(jDoc))).success).toEqual(false); + }); + + it("fails if data === null", async () => { + jDoc.data = null; + expect((await importer.parse(JSON.stringify(jDoc))).success).toEqual(false); + }); + }); + + describe("inner importer", () => { + beforeEach(() => { + cryptoService.decryptToUtf8(Arg.any(), Arg.any()).resolves("successful decryption"); + }); + it("delegates success", async () => { + expect((await importer.parse(JSON.stringify(jDoc))).success).toEqual(true); + result.success = false; + expect((await importer.parse(JSON.stringify(jDoc))).success).toEqual(false); + }); + + it("passes on organization Id", async () => { + jDoc.format = "csv"; + importer.organizationId = Utils.newGuid(); + await importer.parse(JSON.stringify(jDoc)); + + importService.received(1).getImporter("bitwardencsv", importer.organizationId); + }); + + it("passes null organizationId if none set", async () => { + jDoc.format = "csv"; + importer.organizationId = null; + await importer.parse(JSON.stringify(jDoc)); + + importService.received(1).getImporter("bitwardencsv", null); + }); + + it("gets csv importer for csv format", async () => { + jDoc.format = "csv"; + + await importer.parse(JSON.stringify(jDoc)); + + importService.received(1).getImporter("bitwardencsv", Arg.any()); + }); + + it("gets json importer for json format", async () => { + jDoc.format = "json"; + + await importer.parse(JSON.stringify(jDoc)); + + importService.received(1).getImporter("bitwardenjson", Arg.any()); + }); + + it("gets json importer for encrypted_json format", async () => { + jDoc.format = "encrypted_json"; + + await importer.parse(JSON.stringify(jDoc)); + + importService.received(1).getImporter("bitwardenjson", Arg.any()); + }); + }); +}); diff --git a/spec/common/services/export.service.spec.ts b/spec/common/services/export.service.spec.ts index 62402d78f6..cb09326185 100644 --- a/spec/common/services/export.service.spec.ts +++ b/spec/common/services/export.service.spec.ts @@ -1,8 +1,9 @@ -import { Substitute, SubstituteOf } from "@fluffy-spoon/substitute"; +import { Arg, Substitute, SubstituteOf } from "@fluffy-spoon/substitute"; import { ApiService } from "jslib-common/abstractions/api.service"; import { CipherService } from "jslib-common/abstractions/cipher.service"; import { CryptoService } from "jslib-common/abstractions/crypto.service"; +import { CryptoFunctionService } from "jslib-common/abstractions/cryptoFunction.service"; import { FolderService } from "jslib-common/abstractions/folder.service"; import { ExportService } from "jslib-common/services/export.service"; @@ -13,6 +14,9 @@ import { Login } from "jslib-common/models/domain/login"; import { CipherWithIds as CipherExport } from "jslib-common/models/export/cipherWithIds"; import { CipherType } from "jslib-common/enums/cipherType"; +import { KdfType } from "jslib-common/enums/kdfType"; + +import { Utils } from "jslib-common/misc/utils"; import { CipherView } from "jslib-common/models/view/cipherView"; import { LoginView } from "jslib-common/models/view/loginView"; @@ -85,12 +89,14 @@ function expectEqualCiphers(ciphers: CipherView[] | Cipher[], jsonResult: string describe("ExportService", () => { let exportService: ExportService; let apiService: SubstituteOf; + let cryptoFunctionService: SubstituteOf; let cipherService: SubstituteOf; let folderService: SubstituteOf; let cryptoService: SubstituteOf; beforeEach(() => { apiService = Substitute.for(); + cryptoFunctionService = Substitute.for(); cipherService = Substitute.for(); folderService = Substitute.for(); cryptoService = Substitute.for(); @@ -98,7 +104,13 @@ describe("ExportService", () => { folderService.getAllDecrypted().resolves([]); folderService.getAll().resolves([]); - exportService = new ExportService(folderService, cipherService, apiService, cryptoService); + exportService = new ExportService( + folderService, + cipherService, + apiService, + cryptoService, + cryptoFunctionService + ); }); it("exports unecrypted user ciphers", async () => { @@ -132,4 +144,68 @@ describe("ExportService", () => { expectEqualCiphers(UserCipherDomains.slice(0, 2), actual); }); + + describe("password protected export", () => { + let exportString: string; + let exportObject: any; + let mac: SubstituteOf; + let data: SubstituteOf; + const password = "password"; + const salt = "salt"; + + describe("export json object", () => { + beforeEach(async () => { + mac = Substitute.for(); + data = Substitute.for(); + + mac.encryptedString = "mac"; + data.encryptedString = "encData"; + + spyOn(Utils, "fromBufferToB64").and.returnValue(salt); + cipherService.getAllDecrypted().resolves(UserCipherViews.slice(0, 1)); + + exportString = await exportService.getPasswordProtectedExport(password); + exportObject = JSON.parse(exportString); + }); + + it("specifies it is encrypted", () => { + expect(exportObject.encrypted).toBe(true); + }); + + it("specifies it's password protected", () => { + expect(exportObject.passwordProtected).toBe(true); + }); + + it("specifies format", () => { + expect(exportObject).toEqual(jasmine.objectContaining({ format: jasmine.any(String) })); + }); + + it("specifies salt", () => { + expect(exportObject.salt).toEqual("salt"); + }); + + it("specifies kdfIterations", () => { + expect(exportObject.kdfIterations).toEqual(100000); + }); + + it("has kdfType", () => { + expect(exportObject.kdfType).toEqual(KdfType.PBKDF2_SHA256); + }); + + it("has a mac property", () => { + cryptoService.encrypt(Arg.any(), Arg.any()).resolves(mac); + expect(exportObject.encKeyValidation_DO_NOT_EDIT).toEqual(mac.encryptedString); + }); + + it("has data property", () => { + cryptoService.encrypt(Arg.any(), Arg.any()).resolves(data); + expect(exportObject.data).toEqual(data.encryptedString); + }); + + it("encrypts the data property", async () => { + const unencrypted = await exportService.getExport(); + expect(exportObject.data).not.toEqual(unencrypted); + }); + }); + }); }); diff --git a/spec/common/services/import.service.spec.ts b/spec/common/services/import.service.spec.ts new file mode 100644 index 0000000000..602cbc20ae --- /dev/null +++ b/spec/common/services/import.service.spec.ts @@ -0,0 +1,74 @@ +import Substitute, { Arg, SubstituteOf } from "@fluffy-spoon/substitute"; +import { ApiService } from "jslib-common/abstractions/api.service"; + +import { CipherService } from "jslib-common/abstractions/cipher.service"; +import { CollectionService } from "jslib-common/abstractions/collection.service"; +import { CryptoService } from "jslib-common/abstractions/crypto.service"; +import { FolderService } from "jslib-common/abstractions/folder.service"; +import { I18nService } from "jslib-common/abstractions/i18n.service"; +import { PlatformUtilsService } from "jslib-common/abstractions/platformUtils.service"; +import { BitwardenPasswordProtectedImporter } from "jslib-common/importers/bitwardenPasswordProtectedImporter"; + +import { Importer } from "jslib-common/importers/importer"; +import { Utils } from "jslib-common/misc/utils"; + +import { ImportService } from "jslib-common/services/import.service"; + +describe("ImportService", () => { + let importService: ImportService; + let cipherService: SubstituteOf; + let folderService: SubstituteOf; + let apiService: SubstituteOf; + let i18nService: SubstituteOf; + let collectionService: SubstituteOf; + let platformUtilsService: SubstituteOf; + let cryptoService: SubstituteOf; + + beforeEach(() => { + cipherService = Substitute.for(); + folderService = Substitute.for(); + apiService = Substitute.for(); + i18nService = Substitute.for(); + collectionService = Substitute.for(); + platformUtilsService = Substitute.for(); + cryptoService = Substitute.for(); + + importService = new ImportService( + cipherService, + folderService, + apiService, + i18nService, + collectionService, + platformUtilsService, + cryptoService + ); + }); + + describe("getImporterInstance", () => { + describe("Get bitPasswordProtected importer", () => { + let importer: Importer; + const organizationId = Utils.newGuid(); + const password = Utils.newGuid(); + + beforeEach(() => { + importer = importService.getImporter( + "bitwardenpasswordprotected", + organizationId, + password + ); + }); + + it("returns an instance of BitwardenPasswordProtectedImporter", () => { + expect(importer).toBeInstanceOf(BitwardenPasswordProtectedImporter); + }); + + it("has the appropriate organization Id", () => { + expect(importer.organizationId).toEqual(organizationId); + }); + + it("has the appropriate password", () => { + expect(Object.entries(importer)).toEqual(jasmine.arrayContaining([["password", password]])); + }); + }); + }); +});