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
This commit is contained in:
Matt Gibson 2022-02-22 21:02:07 -06:00 committed by GitHub
parent 842d6cd001
commit 1fb3d54014
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
16 changed files with 194 additions and 233 deletions

View File

@ -1,5 +1,6 @@
# Build directories
dist
coverage
# Github Workflows
.github/workflows

View File

@ -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<string>;
getPasswordProtectedExport: (
password: string,
format?: "csv" | "json" | "encrypted_json",
organizationId?: string
) => Promise<string>;
getOrganizationExport: (
organizationId: string,
format?: "csv" | "json" | "encrypted_json"
) => Promise<string>;
getExport: (format?: ExportFormat, organizationId?: string) => Promise<string>;
getPasswordProtectedExport: (password: string, organizationId?: string) => Promise<string>;
getOrganizationExport: (organizationId: string, format?: ExportFormat) => Promise<string>;
getEventExport: (events: EventView[]) => Promise<string>;
getFileName: (prefix?: string, extension?: string) => string;
}

View File

@ -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<Error>;
getImporter: (format: ImportType, organizationId: string, password?: string) => Importer;
import: (
importer: Importer,
fileContents: string,
organizationId?: string
) => Promise<ImportError>;
getImporter: (
format: ImportType | "bitwardenpasswordprotected",
organizationId: string,
password?: string
) => Importer;
}

View File

@ -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"];

View File

@ -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<ImportResult> {
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;
}

View File

@ -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<ImportResult> {
@ -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<boolean> {
@ -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);
}
}

View File

@ -0,0 +1,5 @@
export class ImportError extends Error {
constructor(message?: string, public passwordRequired: boolean = false) {
super(message);
}
}

View File

@ -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<T>(name: string & keyof T) {
return name;
}

View File

@ -4,6 +4,7 @@ import { FolderView } from "../view/folderView";
export class ImportResult {
success = false;
missingPassword = false;
errorMessage: string;
ciphers: CipherView[] = [];
folders: FolderView[] = [];

View File

@ -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<string> {
async getExport(format: ExportFormat = "csv", organizationId?: string): Promise<string> {
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<string> {
async getPasswordProtectedExport(password: string, organizationId?: string): Promise<string> {
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<string> {
if (format === "encrypted_json") {
return this.getOrganizationEncryptedExport(organizationId);

View File

@ -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<Error> {
): Promise<ImportError> {
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);
}
}

View File

@ -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<CryptoService>;
let i18nService: SubstituteOf<I18nService>;
beforeEach(() => {
cryptoService = Substitute.for<CryptoService>();
i18nService = Substitute.for<I18nService>();
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);
});
});

View File

@ -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<Importer>;
let importService: SubstituteOf<ImportService>;
let cryptoService: SubstituteOf<CryptoService>;
let i18nService: SubstituteOf<I18nService>;
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<CryptoService>();
i18nService = Substitute.for<I18nService>();
importService = Substitute.for<ImportService>();
innerImporter = Substitute.for<Importer>();
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());
});
});
});

View File

@ -0,0 +1 @@
export const data = '{"encrypted":false,"folders":[],"items":[]}';

View File

@ -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="
}`;

View File

@ -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");
});