mirror of
https://github.com/bitwarden/browser.git
synced 2025-02-11 00:31:45 +01:00
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:
parent
842d6cd001
commit
1fb3d54014
@ -1,5 +1,6 @@
|
||||
# Build directories
|
||||
dist
|
||||
coverage
|
||||
|
||||
# Github Workflows
|
||||
.github/workflows
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
|
72
common/src/enums/importOptions.ts
Normal file
72
common/src/enums/importOptions.ts
Normal 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"];
|
@ -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;
|
||||
}
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
5
common/src/importers/importError.ts
Normal file
5
common/src/importers/importError.ts
Normal file
@ -0,0 +1,5 @@
|
||||
export class ImportError extends Error {
|
||||
constructor(message?: string, public passwordRequired: boolean = false) {
|
||||
super(message);
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
|
@ -4,6 +4,7 @@ import { FolderView } from "../view/folderView";
|
||||
|
||||
export class ImportResult {
|
||||
success = false;
|
||||
missingPassword = false;
|
||||
errorMessage: string;
|
||||
ciphers: CipherView[] = [];
|
||||
folders: FolderView[] = [];
|
||||
|
@ -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);
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
31
spec/common/importers/bitwardenJsonImporter.spec.ts
Normal file
31
spec/common/importers/bitwardenJsonImporter.spec.ts
Normal 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);
|
||||
});
|
||||
});
|
@ -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());
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@ -0,0 +1 @@
|
||||
export const data = '{"encrypted":false,"folders":[],"items":[]}';
|
@ -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="
|
||||
}`;
|
@ -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");
|
||||
});
|
||||
|
Loading…
Reference in New Issue
Block a user