From 1fb3d54014ffaabdc532e55ffa68ebb8a1a25683 Mon Sep 17 00:00:00 2001 From: Matt Gibson Date: Tue, 22 Feb 2022 21:02:07 -0600 Subject: [PATCH] Feature/password protected export (#689) * Simplify password protected file format * no items to import is not an error * Await inner importer * Add export format type * Error if import file is password protected * Update tests * Test password protected with normat json importer * Simplify imports * Ignore code coverage directory * Expand importer options without changing display options * Import password require import error handling * Use interface * Fix curlies * linter fixes * Add null of empty util * Lint fixes * run prettier * Move import options to separate enum file * Fix imports --- .prettierignore | 1 + common/src/abstractions/export.service.ts | 15 +-- common/src/abstractions/import.service.ts | 19 ++-- common/src/enums/importOptions.ts | 72 ++++++++++++ common/src/importers/bitwardenJsonImporter.ts | 11 +- .../bitwardenPasswordProtectedImporter.ts | 30 +---- common/src/importers/importError.ts | 5 + common/src/misc/utils.ts | 4 + common/src/models/domain/importResult.ts | 1 + common/src/services/export.service.ts | 24 ++-- common/src/services/import.service.ts | 105 ++++-------------- .../importers/bitwardenJsonImporter.spec.ts | 31 ++++++ ...bitwardenPasswordProtectedImporter.spec.ts | 95 +--------------- .../testData/bitwardenJson/empty.json.ts | 1 + .../bitwardenJson/passwordProtected.json.ts | 9 ++ spec/common/services/export.service.spec.ts | 4 - 16 files changed, 194 insertions(+), 233 deletions(-) create mode 100644 common/src/enums/importOptions.ts create mode 100644 common/src/importers/importError.ts create mode 100644 spec/common/importers/bitwardenJsonImporter.spec.ts create mode 100644 spec/common/importers/testData/bitwardenJson/empty.json.ts create mode 100644 spec/common/importers/testData/bitwardenJson/passwordProtected.json.ts diff --git a/.prettierignore b/.prettierignore index 2d0a1ff569..17741a6533 100644 --- a/.prettierignore +++ b/.prettierignore @@ -1,5 +1,6 @@ # Build directories dist +coverage # Github Workflows .github/workflows diff --git a/common/src/abstractions/export.service.ts b/common/src/abstractions/export.service.ts index 1d3fbce758..b0266530bd 100644 --- a/common/src/abstractions/export.service.ts +++ b/common/src/abstractions/export.service.ts @@ -1,16 +1,11 @@ import { EventView } from "../models/view/eventView"; +export type ExportFormat = "csv" | "json" | "encrypted_json"; + 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" - ) => Promise; + getExport: (format?: ExportFormat, organizationId?: string) => Promise; + getPasswordProtectedExport: (password: string, organizationId?: string) => Promise; + getOrganizationExport: (organizationId: string, format?: ExportFormat) => Promise; getEventExport: (events: EventView[]) => Promise; getFileName: (prefix?: string, extension?: string) => string; } diff --git a/common/src/abstractions/import.service.ts b/common/src/abstractions/import.service.ts index 799ddd92a2..01f099b318 100644 --- a/common/src/abstractions/import.service.ts +++ b/common/src/abstractions/import.service.ts @@ -1,14 +1,19 @@ +import { ImportOption, ImportType } from "../enums/importOptions"; +import { ImportError } from "../importers/importError"; import { Importer } from "../importers/importer"; -import { ImportType } from "../services/import.service"; -export interface ImportOption { - id: string; - name: string; -} export abstract class ImportService { featuredImportOptions: readonly ImportOption[]; regularImportOptions: readonly ImportOption[]; getImportOptions: () => ImportOption[]; - import: (importer: Importer, fileContents: string, organizationId?: string) => Promise; - getImporter: (format: ImportType, organizationId: string, password?: string) => Importer; + import: ( + importer: Importer, + fileContents: string, + organizationId?: string + ) => Promise; + getImporter: ( + format: ImportType | "bitwardenpasswordprotected", + organizationId: string, + password?: string + ) => Importer; } diff --git a/common/src/enums/importOptions.ts b/common/src/enums/importOptions.ts new file mode 100644 index 0000000000..5b690196d9 --- /dev/null +++ b/common/src/enums/importOptions.ts @@ -0,0 +1,72 @@ +export interface ImportOption { + id: string; + name: string; +} + +export 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; + +export 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"]; diff --git a/common/src/importers/bitwardenJsonImporter.ts b/common/src/importers/bitwardenJsonImporter.ts index 52a4156b37..6caf185efb 100644 --- a/common/src/importers/bitwardenJsonImporter.ts +++ b/common/src/importers/bitwardenJsonImporter.ts @@ -13,14 +13,21 @@ export class BitwardenJsonImporter extends BaseImporter implements Importer { private results: any; private result: ImportResult; - constructor(private cryptoService: CryptoService, private i18nService: I18nService) { + constructor(protected cryptoService: CryptoService, protected i18nService: I18nService) { super(); } async parse(data: string): Promise { this.result = new ImportResult(); this.results = JSON.parse(data); - if (this.results == null || this.results.items == null || this.results.items.length === 0) { + if (this.results == null || this.results.items == null) { + if (this.results?.passwordProtected) { + this.result.success = false; + this.result.missingPassword = true; + this.result.errorMessage = this.i18nService.t("importPasswordRequired"); + return this.result; + } + this.result.success = false; return this.result; } diff --git a/common/src/importers/bitwardenPasswordProtectedImporter.ts b/common/src/importers/bitwardenPasswordProtectedImporter.ts index a08b36da81..54da71b5e5 100644 --- a/common/src/importers/bitwardenPasswordProtectedImporter.ts +++ b/common/src/importers/bitwardenPasswordProtectedImporter.ts @@ -1,18 +1,16 @@ import { CryptoService } from "../abstractions/crypto.service"; import { I18nService } from "../abstractions/i18n.service"; -import { ImportService } from "../abstractions/import.service"; import { KdfType } from "../enums/kdfType"; import { EncString } from "../models/domain/encString"; import { ImportResult } from "../models/domain/importResult"; import { SymmetricCryptoKey } from "../models/domain/symmetricCryptoKey"; -import { BaseImporter } from "./baseImporter"; +import { BitwardenJsonImporter } from "./bitwardenJsonImporter"; import { Importer } from "./importer"; -class BitwardenPasswordProtectedFileFormat { +interface BitwardenPasswordProtectedFileFormat { encrypted: boolean; passwordProtected: boolean; - format: "json" | "csv" | "encrypted_json"; salt: string; kdfIterations: number; kdfType: number; @@ -20,17 +18,11 @@ class BitwardenPasswordProtectedFileFormat { data: string; } -export class BitwardenPasswordProtectedImporter extends BaseImporter implements Importer { - private innerImporter: Importer; +export class BitwardenPasswordProtectedImporter extends BitwardenJsonImporter implements Importer { private key: SymmetricCryptoKey; - constructor( - private importService: ImportService, - private cryptoService: CryptoService, - private i18nService: I18nService, - private password: string - ) { - super(); + constructor(cryptoService: CryptoService, i18nService: I18nService, private password: string) { + super(cryptoService, i18nService); } async parse(data: string): Promise { @@ -41,8 +33,6 @@ export class BitwardenPasswordProtectedImporter extends BaseImporter implements return result; } - this.setInnerImporter(parsedData.format); - if (!(await this.checkPassword(parsedData))) { result.success = false; result.errorMessage = this.i18nService.t("importEncKeyError"); @@ -51,7 +41,7 @@ export class BitwardenPasswordProtectedImporter extends BaseImporter implements const encData = new EncString(parsedData.data); const clearTextData = await this.cryptoService.decryptToUtf8(encData, this.key); - return this.innerImporter.parse(clearTextData); + return await super.parse(clearTextData); } private async checkPassword(jdoc: BitwardenPasswordProtectedFileFormat): Promise { @@ -79,7 +69,6 @@ export class BitwardenPasswordProtectedImporter extends BaseImporter implements !jdoc || !jdoc.encrypted || !jdoc.passwordProtected || - !(jdoc.format === "csv" || jdoc.format === "json" || jdoc.format === "encrypted_json") || !jdoc.salt || !jdoc.kdfIterations || typeof jdoc.kdfIterations !== "number" || @@ -89,11 +78,4 @@ export class BitwardenPasswordProtectedImporter extends BaseImporter implements !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/importers/importError.ts b/common/src/importers/importError.ts new file mode 100644 index 0000000000..6a74d66e9f --- /dev/null +++ b/common/src/importers/importError.ts @@ -0,0 +1,5 @@ +export class ImportError extends Error { + constructor(message?: string, public passwordRequired: boolean = false) { + super(message); + } +} diff --git a/common/src/misc/utils.ts b/common/src/misc/utils.ts index 409c4c7470..1d96e08287 100644 --- a/common/src/misc/utils.ts +++ b/common/src/misc/utils.ts @@ -307,6 +307,10 @@ export class Utils { return str == null || typeof str !== "string" || str.trim() === ""; } + static isNullOrEmpty(str: string): boolean { + return str == null || typeof str !== "string" || str == ""; + } + static nameOf(name: string & keyof T) { return name; } diff --git a/common/src/models/domain/importResult.ts b/common/src/models/domain/importResult.ts index 1ac2816138..695dbf9d4a 100644 --- a/common/src/models/domain/importResult.ts +++ b/common/src/models/domain/importResult.ts @@ -4,6 +4,7 @@ import { FolderView } from "../view/folderView"; export class ImportResult { success = false; + missingPassword = false; errorMessage: string; ciphers: CipherView[] = []; folders: FolderView[] = []; diff --git a/common/src/services/export.service.ts b/common/src/services/export.service.ts index 7bbd574b4c..ddece8eb34 100644 --- a/common/src/services/export.service.ts +++ b/common/src/services/export.service.ts @@ -4,7 +4,10 @@ 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 { + ExportFormat, + ExportService as ExportServiceAbstraction, +} from "../abstractions/export.service"; import { FolderService } from "../abstractions/folder.service"; import { CipherType } from "../enums/cipherType"; import { KdfType } from "../enums/kdfType"; @@ -33,7 +36,11 @@ export class ExportService implements ExportServiceAbstraction { private cryptoFunctionService: CryptoFunctionService ) {} - async getExport(format: "csv" | "json" | "encrypted_json" = "csv"): Promise { + async getExport(format: ExportFormat = "csv", organizationId?: string): Promise { + if (organizationId) { + return await this.getOrganizationExport(organizationId, format); + } + if (format === "encrypted_json") { return this.getEncryptedExport(); } else { @@ -41,14 +48,10 @@ export class ExportService implements ExportServiceAbstraction { } } - async getPasswordProtectedExport( - password: string, - format: "csv" | "json" | "encrypted_json" = "csv", - organizationId?: string - ): Promise { + async getPasswordProtectedExport(password: string, organizationId?: string): Promise { const clearText = organizationId - ? await this.getOrganizationExport(organizationId, format) - : await this.getExport(format); + ? await this.getOrganizationExport(organizationId, "json") + : await this.getExport("json"); const salt = Utils.fromBufferToB64(await this.cryptoFunctionService.randomBytes(16)); const kdfIterations = 100000; @@ -65,7 +68,6 @@ export class ExportService implements ExportServiceAbstraction { const jsonDoc: any = { encrypted: true, passwordProtected: true, - format: format, salt: salt, kdfIterations: kdfIterations, kdfType: KdfType.PBKDF2_SHA256, @@ -78,7 +80,7 @@ export class ExportService implements ExportServiceAbstraction { async getOrganizationExport( organizationId: string, - format: "csv" | "json" | "encrypted_json" = "csv" + format: ExportFormat = "csv" ): Promise { if (format === "encrypted_json") { return this.getOrganizationEncryptedExport(organizationId); diff --git a/common/src/services/import.service.ts b/common/src/services/import.service.ts index 4e122932cc..3913074d0d 100644 --- a/common/src/services/import.service.ts +++ b/common/src/services/import.service.ts @@ -4,12 +4,15 @@ import { CollectionService } from "../abstractions/collection.service"; import { CryptoService } from "../abstractions/crypto.service"; import { FolderService } from "../abstractions/folder.service"; import { I18nService } from "../abstractions/i18n.service"; -import { - ImportOption, - ImportService as ImportServiceAbstraction, -} from "../abstractions/import.service"; +import { ImportService as ImportServiceAbstraction } from "../abstractions/import.service"; import { PlatformUtilsService } from "../abstractions/platformUtils.service"; import { CipherType } from "../enums/cipherType"; +import { + featuredImportOptions, + ImportOption, + ImportType, + regularImportOptions, +} from "../enums/importOptions"; import { AscendoCsvImporter } from "../importers/ascendoCsvImporter"; import { AvastCsvImporter } from "../importers/avastCsvImporter"; import { AvastJsonImporter } from "../importers/avastJsonImporter"; @@ -30,6 +33,7 @@ import { EnpassJsonImporter } from "../importers/enpassJsonImporter"; import { FirefoxCsvImporter } from "../importers/firefoxCsvImporter"; import { FSecureFskImporter } from "../importers/fsecureFskImporter"; import { GnomeJsonImporter } from "../importers/gnomeJsonImporter"; +import { ImportError } from "../importers/importError"; import { Importer } from "../importers/importer"; import { KasperskyTxtImporter } from "../importers/kasperskyTxtImporter"; import { KeePass2XmlImporter } from "../importers/keepass2XmlImporter"; @@ -76,75 +80,6 @@ import { KvpRequest } from "../models/request/kvpRequest"; import { ErrorResponse } from "../models/response/errorResponse"; import { CipherView } from "../models/view/cipherView"; -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; - -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[]; @@ -168,11 +103,11 @@ export class ImportService implements ImportServiceAbstraction { importer: Importer, fileContents: string, organizationId: string = null - ): Promise { + ): Promise { const importResult = await importer.parse(fileContents); if (importResult.success) { if (importResult.folders.length === 0 && importResult.ciphers.length === 0) { - return new Error(this.i18nService.t("importNothingError")); + return new ImportError(this.i18nService.t("importNothingError")); } else if (importResult.ciphers.length > 0) { const halfway = Math.floor(importResult.ciphers.length / 2); const last = importResult.ciphers.length - 1; @@ -182,7 +117,7 @@ export class ImportService implements ImportServiceAbstraction { this.badData(importResult.ciphers[halfway]) && this.badData(importResult.ciphers[last]) ) { - return new Error(this.i18nService.t("importFormatError")); + return new ImportError(this.i18nService.t("importFormatError")); } } try { @@ -194,15 +129,18 @@ export class ImportService implements ImportServiceAbstraction { return null; } else { if (!Utils.isNullOrWhitespace(importResult.errorMessage)) { - return new Error(importResult.errorMessage); + return new ImportError(importResult.errorMessage, importResult.missingPassword); } else { - return new Error(this.i18nService.t("importFormatError")); + return new ImportError( + this.i18nService.t("importFormatError"), + importResult.missingPassword + ); } } } getImporter( - format: ImportType, + format: ImportType | "bitwardenpasswordprotected", organizationId: string = null, password: string = null ): Importer { @@ -214,7 +152,7 @@ export class ImportService implements ImportServiceAbstraction { return importer; } - private getImporterInstance(format: ImportType, password: string) { + private getImporterInstance(format: ImportType | "bitwardenpasswordprotected", password: string) { if (format == null) { return null; } @@ -226,7 +164,6 @@ export class ImportService implements ImportServiceAbstraction { return new BitwardenJsonImporter(this.cryptoService, this.i18nService); case "bitwardenpasswordprotected": return new BitwardenPasswordProtectedImporter( - this, this.cryptoService, this.i18nService, password @@ -394,9 +331,9 @@ export class ImportService implements ImportServiceAbstraction { ); } - private handleServerError(errorResponse: ErrorResponse, importResult: ImportResult): Error { + private handleServerError(errorResponse: ErrorResponse, importResult: ImportResult): ImportError { if (errorResponse.validationErrors == null) { - return new Error(errorResponse.message); + return new ImportError(errorResponse.message); } let errorMessage = ""; @@ -434,6 +371,6 @@ export class ImportService implements ImportServiceAbstraction { errorMessage += "[" + itemType + '] "' + item.name + '": ' + value; }); - return new Error(errorMessage); + return new ImportError(errorMessage); } } diff --git a/spec/common/importers/bitwardenJsonImporter.spec.ts b/spec/common/importers/bitwardenJsonImporter.spec.ts new file mode 100644 index 0000000000..94b035286b --- /dev/null +++ b/spec/common/importers/bitwardenJsonImporter.spec.ts @@ -0,0 +1,31 @@ +import { Substitute, SubstituteOf } from "@fluffy-spoon/substitute"; + +import { CryptoService } from "jslib-common/abstractions/crypto.service"; +import { I18nService } from "jslib-common/abstractions/i18n.service"; +import { BitwardenJsonImporter } from "jslib-common/importers/bitwardenJsonImporter"; + +import { data as passwordProtectedData } from "./testData/bitwardenJson/passwordProtected.json"; + +describe("bitwarden json importer", () => { + let sut: BitwardenJsonImporter; + let cryptoService: SubstituteOf; + let i18nService: SubstituteOf; + + beforeEach(() => { + cryptoService = Substitute.for(); + i18nService = Substitute.for(); + + sut = new BitwardenJsonImporter(cryptoService, i18nService); + }); + + it("should fail if password is needed", async () => { + expect((await sut.parse(passwordProtectedData)).success).toBe(false); + }); + + it("should return password needed error message", async () => { + const expected = "Password required error message"; + i18nService.t("importPasswordRequired").returns(expected); + + expect((await sut.parse(passwordProtectedData)).errorMessage).toEqual(expected); + }); +}); diff --git a/spec/common/importers/bitwardenPasswordProtectedImporter.spec.ts b/spec/common/importers/bitwardenPasswordProtectedImporter.spec.ts index cacfa3d23e..ed2434ee53 100644 --- a/spec/common/importers/bitwardenPasswordProtectedImporter.spec.ts +++ b/spec/common/importers/bitwardenPasswordProtectedImporter.spec.ts @@ -2,17 +2,15 @@ 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"; +import { data as emptyDecryptedData } from "./testData/bitwardenJson/empty.json"; + describe("BitwardenPasswordProtectedImporter", () => { let importer: BitwardenPasswordProtectedImporter; - let innerImporter: SubstituteOf; - let importService: SubstituteOf; let cryptoService: SubstituteOf; let i18nService: SubstituteOf; const password = Utils.newGuid(); @@ -20,7 +18,6 @@ describe("BitwardenPasswordProtectedImporter", () => { let jDoc: { encrypted?: boolean; passwordProtected?: boolean; - format?: string; salt?: string; kdfIterations?: any; kdfType?: any; @@ -31,13 +28,10 @@ describe("BitwardenPasswordProtectedImporter", () => { 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, @@ -46,32 +40,12 @@ describe("BitwardenPasswordProtectedImporter", () => { }; result.success = true; - innerImporter.parse(Arg.any()).resolves(result); - importer = new BitwardenPasswordProtectedImporter( - importService, - cryptoService, - i18nService, - password - ); + importer = new BitwardenPasswordProtectedImporter(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"); + cryptoService.decryptToUtf8(Arg.any(), Arg.any()).resolves(emptyDecryptedData); expect((await importer.parse(JSON.stringify(jDoc))).success).toEqual(true); }); @@ -96,16 +70,6 @@ describe("BitwardenPasswordProtectedImporter", () => { 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); @@ -146,55 +110,4 @@ describe("BitwardenPasswordProtectedImporter", () => { 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/importers/testData/bitwardenJson/empty.json.ts b/spec/common/importers/testData/bitwardenJson/empty.json.ts new file mode 100644 index 0000000000..43ab8c763a --- /dev/null +++ b/spec/common/importers/testData/bitwardenJson/empty.json.ts @@ -0,0 +1 @@ +export const data = '{"encrypted":false,"folders":[],"items":[]}'; diff --git a/spec/common/importers/testData/bitwardenJson/passwordProtected.json.ts b/spec/common/importers/testData/bitwardenJson/passwordProtected.json.ts new file mode 100644 index 0000000000..0462e83423 --- /dev/null +++ b/spec/common/importers/testData/bitwardenJson/passwordProtected.json.ts @@ -0,0 +1,9 @@ +export const data = `{ + "encrypted": true, + "passwordProtected": true, + "salt": "Oy0xcgVRzxQ+9NpB5GLehw==", + "kdfIterations": 100000, + "kdfType": 0, + "encKeyValidation_DO_NOT_EDIT": "2.sZs4Jc1HW9rhABzRRYR/gQ==|8kTDaDxafulnybpWoqVX8RAybhVRTr+dffNjms271Y7amQmIE1VSMwLbk+b2vxZb|IqOo6oXQtmv/Xb/GHDi42XG9c9ILePYtP5qq584VWcg=", + "data": "2.D0AXAf7G/XIwq6EC7A0Suw==|4w+m0wHRo25y1T1Syh5wdAUyF8voqEy54waMEsbnK0Nzee959w54ru5D1NntvxZL4HFqkQLyR6jCFkn5g40f+MGJgihS/wvf4NcJJfLiiFo6MEDOQNBkxw7ZBGuHiKfVuBO5u36JgzQtZ8lyFaduGxFszuF5c+URiE9PDh9jY0//poVgHKwuLZuYFIW+f7h6T+shUWK0ya11lcHn/B/CA2xiI+YiKdNZreJrwN0yslpJ/f+MrOzagvftRjt0GNkwveCtwcYUw/zFvqvibUpKeHcRiXs8SaGoHJ5RTm69FbJ7C5tnLwoVT89Af156uvRAXV7yAC4oPcbU/3TGb6hqYosvi1QNyaqG3M9gxS6+AK0C4yWuNbMLDEr+MWiw0SWLVMKQEkCZ4oM+oTCx52otW3+2V9I8Pv3KmmhkvVvE4wBdweOJeRX53Tf5ySkmpIhCfzj6JMmxO+nmTXIhWnJChr4hPVh+ixv1GQK5thIPTCMXmAtXoTIFUx1KWjS6LjOdi2hKQueVI+XZjf0qnY2vTMxRg0ZsLBA2znQTx+DSEqumORb5T/lV73pWZiCNePSAE2msOm7tep+lm4O/VCViCfXjITAY196syhOK0XnhxJvPALchZY8sYRAfuw6hHoDiVr+JUieRoI7eUrhXBp+D6Py9TL/dS/rHe+C2Zhx+xwx2NfGt+xEp8ZAOOCxgZ0UTeSA/abm0Oz7tJIK1n26acQrgbr7rMeBymAX+5L5OWlwI1hGgEBfj6W0rrbSXf3VMfaFXZ5UsXi1VhzQmU3LyWENoDeImXFQj6zMbUSfcVwLsG5Fg8Ee/kO/wJPfG5BO51+/vFqQj6AkaMEcwg5xNrObHYfQ/DMhIn7YDM2zdzbNTdhnobGkz6YRKFPCgFe3EmIEPEpeh9S3eKE9C7MQsrR8jVSiseR/FipJLsN+W7iOwzeXdwxUFlC/0a98bTKvdrbMgNi6ZVXykHY/t2UyEGpxZGTHoZwhX01kiQrwzC4/+v/676ldxPluO9GY7MtrLveCDsiyBz15u43IGHayDEBNT0rqrOKLYmfzwCWoahRLZQrSmepe/FXqgPqRfyWc/Ro+w3sT9dXUkx3B5xxWgSyABowPV48yBUSJuefhKTpqgzkU+LzhNnWHjnxJzzQ2/|IhlRjnyhIoDM85qHX/bY2zaIU5YaRO/iFVTQDd3uFDo=" +}`; diff --git a/spec/common/services/export.service.spec.ts b/spec/common/services/export.service.spec.ts index e0d019940c..a3a0299ec7 100644 --- a/spec/common/services/export.service.spec.ts +++ b/spec/common/services/export.service.spec.ts @@ -172,10 +172,6 @@ describe("ExportService", () => { 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"); });