1
0
mirror of https://github.com/bitwarden/browser synced 2025-12-15 15:53:27 +00:00

Feature/password protected export (#612)

* Add password protected export

* Run prettier

* Test password protected export service

* Create type for known import type strings

* Test import service changes

* Test bitwarden password importer

* Run prettier

* Remove unnecessary class properties

* Run prettier

* Tslint fixes

* Add KdfType to password protected export

* Linter fixes

* run prettier
This commit is contained in:
Matt Gibson
2022-02-07 10:33:10 -05:00
committed by GitHub
parent 9caea70ea2
commit 7afb748791
8 changed files with 594 additions and 75 deletions

View File

@@ -2,6 +2,11 @@ import { EventView } from "../models/view/eventView";
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"

View File

@@ -1,13 +1,14 @@
import { Importer } from "../importers/importer";
import { ImportType } from "../services/import.service";
export interface ImportOption {
id: string;
name: string;
}
export abstract class ImportService {
featuredImportOptions: ImportOption[];
regularImportOptions: ImportOption[];
featuredImportOptions: readonly ImportOption[];
regularImportOptions: readonly ImportOption[];
getImportOptions: () => ImportOption[];
import: (importer: Importer, fileContents: string, organizationId?: string) => Promise<Error>;
getImporter: (format: string, organizationId: string) => Importer;
getImporter: (format: ImportType, organizationId: string, password?: string) => Importer;
}

View File

@@ -0,0 +1,101 @@
import { BaseImporter } from "./baseImporter";
import { Importer } from "./importer";
import { EncString } from "../models/domain/encString";
import { ImportResult } from "../models/domain/importResult";
import { CryptoService } from "../abstractions/crypto.service";
import { I18nService } from "../abstractions/i18n.service";
import { ImportService } from "../abstractions/import.service";
import { KdfType } from "../enums/kdfType";
import { SymmetricCryptoKey } from "../models/domain/symmetricCryptoKey";
class BitwardenPasswordProtectedFileFormat {
encrypted: boolean;
passwordProtected: boolean;
format: "json" | "csv" | "encrypted_json";
salt: string;
kdfIterations: number;
kdfType: number;
// tslint:disable-next-line
encKeyValidation_DO_NOT_EDIT: string;
data: string;
}
export class BitwardenPasswordProtectedImporter extends BaseImporter implements Importer {
private innerImporter: Importer;
private key: SymmetricCryptoKey;
constructor(
private importService: ImportService,
private cryptoService: CryptoService,
private i18nService: I18nService,
private password: string
) {
super();
}
async parse(data: string): Promise<ImportResult> {
const result = new ImportResult();
const parsedData = JSON.parse(data);
if (this.cannotParseFile(parsedData)) {
result.success = false;
return result;
}
this.setInnerImporter(parsedData.format);
if (!(await this.checkPassword(parsedData))) {
result.success = false;
result.errorMessage = this.i18nService.t("importEncKeyError");
return result;
}
const encData = new EncString(parsedData.data);
const clearTextData = await this.cryptoService.decryptToUtf8(encData, this.key);
return this.innerImporter.parse(clearTextData);
}
private async checkPassword(jdoc: BitwardenPasswordProtectedFileFormat): Promise<boolean> {
this.key = await this.cryptoService.makePinKey(
this.password,
jdoc.salt,
KdfType.PBKDF2_SHA256,
jdoc.kdfIterations
);
const encKeyValidation = new EncString(jdoc.encKeyValidation_DO_NOT_EDIT);
const encKeyValidationDecrypt = await this.cryptoService.decryptToUtf8(
encKeyValidation,
this.key
);
if (encKeyValidationDecrypt === null) {
return false;
}
return true;
}
private cannotParseFile(jdoc: BitwardenPasswordProtectedFileFormat): boolean {
return (
!jdoc ||
!jdoc.encrypted ||
!jdoc.passwordProtected ||
!(jdoc.format === "csv" || jdoc.format === "json" || jdoc.format === "encrypted_json") ||
!jdoc.salt ||
!jdoc.kdfIterations ||
typeof jdoc.kdfIterations !== "number" ||
jdoc.kdfType == null ||
KdfType[jdoc.kdfType] == null ||
!jdoc.encKeyValidation_DO_NOT_EDIT ||
!jdoc.data
);
}
private setInnerImporter(format: "csv" | "json" | "encrypted_json") {
this.innerImporter =
format === "csv"
? this.importService.getImporter("bitwardencsv", this.organizationId)
: this.importService.getImporter("bitwardenjson", this.organizationId);
}
}

View File

@@ -1,10 +1,12 @@
import * as papa from "papaparse";
import { CipherType } from "../enums/cipherType";
import { KdfType } from "../enums/kdfType";
import { ApiService } from "../abstractions/api.service";
import { CipherService } from "../abstractions/cipher.service";
import { CryptoService } from "../abstractions/crypto.service";
import { CryptoFunctionService } from "../abstractions/cryptoFunction.service";
import { ExportService as ExportServiceAbstraction } from "../abstractions/export.service";
import { FolderService } from "../abstractions/folder.service";
@@ -33,7 +35,8 @@ export class ExportService implements ExportServiceAbstraction {
private folderService: FolderService,
private cipherService: CipherService,
private apiService: ApiService,
private cryptoService: CryptoService
private cryptoService: CryptoService,
private cryptoFunctionService: CryptoFunctionService
) {}
async getExport(format: "csv" | "json" | "encrypted_json" = "csv"): Promise<string> {
@@ -44,6 +47,41 @@ export class ExportService implements ExportServiceAbstraction {
}
}
async getPasswordProtectedExport(
password: string,
format: "csv" | "json" | "encrypted_json" = "csv",
organizationId?: string
): Promise<string> {
const clearText = organizationId
? await this.getOrganizationExport(organizationId, format)
: await this.getExport(format);
const salt = Utils.fromBufferToB64(await this.cryptoFunctionService.randomBytes(16));
const kdfIterations = 100000;
const key = await this.cryptoService.makePinKey(
password,
salt,
KdfType.PBKDF2_SHA256,
kdfIterations
);
const encKeyValidation = await this.cryptoService.encrypt(Utils.newGuid(), key);
const encText = await this.cryptoService.encrypt(clearText, key);
const jsonDoc: any = {
encrypted: true,
passwordProtected: true,
format: format,
salt: salt,
kdfIterations: kdfIterations,
kdfType: KdfType.PBKDF2_SHA256,
encKeyValidation_DO_NOT_EDIT: encKeyValidation.encryptedString,
data: encText.encryptedString,
};
return JSON.stringify(jsonDoc, null, " ");
}
async getOrganizationExport(
organizationId: string,
format: "csv" | "json" | "encrypted_json" = "csv"

View File

@@ -32,6 +32,7 @@ import { AvastJsonImporter } from "../importers/avastJsonImporter";
import { AviraCsvImporter } from "../importers/aviraCsvImporter";
import { BitwardenCsvImporter } from "../importers/bitwardenCsvImporter";
import { BitwardenJsonImporter } from "../importers/bitwardenJsonImporter";
import { BitwardenPasswordProtectedImporter } from "../importers/bitwardenPasswordProtectedImporter";
import { BlackBerryCsvImporter } from "../importers/blackBerryCsvImporter";
import { BlurCsvImporter } from "../importers/blurCsvImporter";
import { ButtercupCsvImporter } from "../importers/buttercupCsvImporter";
@@ -82,70 +83,79 @@ import { UpmCsvImporter } from "../importers/upmCsvImporter";
import { YotiCsvImporter } from "../importers/yotiCsvImporter";
import { ZohoVaultCsvImporter } from "../importers/zohoVaultCsvImporter";
export class ImportService implements ImportServiceAbstraction {
featuredImportOptions = [
{ id: "bitwardenjson", name: "Bitwarden (json)" },
{ id: "bitwardencsv", name: "Bitwarden (csv)" },
{ id: "chromecsv", name: "Chrome (csv)" },
{ id: "dashlanejson", name: "Dashlane (json)" },
{ id: "firefoxcsv", name: "Firefox (csv)" },
{ id: "keepass2xml", name: "KeePass 2 (xml)" },
{ id: "lastpasscsv", name: "LastPass (csv)" },
{ id: "safaricsv", name: "Safari and macOS (csv)" },
{ id: "1password1pif", name: "1Password (1pif)" },
];
const featuredImportOptions = [
{ id: "bitwardenjson", name: "Bitwarden (json)" },
{ id: "bitwardencsv", name: "Bitwarden (csv)" },
{ id: "chromecsv", name: "Chrome (csv)" },
{ id: "dashlanejson", name: "Dashlane (json)" },
{ id: "firefoxcsv", name: "Firefox (csv)" },
{ id: "keepass2xml", name: "KeePass 2 (xml)" },
{ id: "lastpasscsv", name: "LastPass (csv)" },
{ id: "safaricsv", name: "Safari and macOS (csv)" },
{ id: "1password1pif", name: "1Password (1pif)" },
] as const;
regularImportOptions: ImportOption[] = [
{ id: "keepassxcsv", name: "KeePassX (csv)" },
{ id: "1passwordwincsv", name: "1Password 6 and 7 Windows (csv)" },
{ id: "1passwordmaccsv", name: "1Password 6 and 7 Mac (csv)" },
{ id: "roboformcsv", name: "RoboForm (csv)" },
{ id: "keepercsv", name: "Keeper (csv)" },
// Temporarily remove this option for the Feb release
// { id: "keeperjson", name: "Keeper (json)" },
{ id: "enpasscsv", name: "Enpass (csv)" },
{ id: "enpassjson", name: "Enpass (json)" },
{ id: "safeincloudxml", name: "SafeInCloud (xml)" },
{ id: "pwsafexml", name: "Password Safe (xml)" },
{ id: "stickypasswordxml", name: "Sticky Password (xml)" },
{ id: "msecurecsv", name: "mSecure (csv)" },
{ id: "truekeycsv", name: "True Key (csv)" },
{ id: "passwordbossjson", name: "Password Boss (json)" },
{ id: "zohovaultcsv", name: "Zoho Vault (csv)" },
{ id: "splashidcsv", name: "SplashID (csv)" },
{ id: "passworddragonxml", name: "Password Dragon (xml)" },
{ id: "padlockcsv", name: "Padlock (csv)" },
{ id: "passboltcsv", name: "Passbolt (csv)" },
{ id: "clipperzhtml", name: "Clipperz (html)" },
{ id: "aviracsv", name: "Avira (csv)" },
{ id: "saferpasscsv", name: "SaferPass (csv)" },
{ id: "upmcsv", name: "Universal Password Manager (csv)" },
{ id: "ascendocsv", name: "Ascendo DataVault (csv)" },
{ id: "meldiumcsv", name: "Meldium (csv)" },
{ id: "passkeepcsv", name: "PassKeep (csv)" },
{ id: "operacsv", name: "Opera (csv)" },
{ id: "vivaldicsv", name: "Vivaldi (csv)" },
{ id: "gnomejson", name: "GNOME Passwords and Keys/Seahorse (json)" },
{ id: "blurcsv", name: "Blur (csv)" },
{ id: "passwordagentcsv", name: "Password Agent (csv)" },
{ id: "passpackcsv", name: "Passpack (csv)" },
{ id: "passmanjson", name: "Passman (json)" },
{ id: "avastcsv", name: "Avast Passwords (csv)" },
{ id: "avastjson", name: "Avast Passwords (json)" },
{ id: "fsecurefsk", name: "F-Secure KEY (fsk)" },
{ id: "kasperskytxt", name: "Kaspersky Password Manager (txt)" },
{ id: "remembearcsv", name: "RememBear (csv)" },
{ id: "passwordwallettxt", name: "PasswordWallet (txt)" },
{ id: "mykicsv", name: "Myki (csv)" },
{ id: "securesafecsv", name: "SecureSafe (csv)" },
{ id: "logmeoncecsv", name: "LogMeOnce (csv)" },
{ id: "blackberrycsv", name: "BlackBerry Password Keeper (csv)" },
{ id: "buttercupcsv", name: "Buttercup (csv)" },
{ id: "codebookcsv", name: "Codebook (csv)" },
{ id: "encryptrcsv", name: "Encryptr (csv)" },
{ id: "yoticsv", name: "Yoti (csv)" },
{ id: "nordpasscsv", name: "Nordpass (csv)" },
];
const regularImportOptions = [
{ id: "keepassxcsv", name: "KeePassX (csv)" },
{ id: "1passwordwincsv", name: "1Password 6 and 7 Windows (csv)" },
{ id: "1passwordmaccsv", name: "1Password 6 and 7 Mac (csv)" },
{ id: "roboformcsv", name: "RoboForm (csv)" },
{ id: "keepercsv", name: "Keeper (csv)" },
// Temporarily remove this option for the Feb release
// { id: "keeperjson", name: "Keeper (json)" },
{ id: "enpasscsv", name: "Enpass (csv)" },
{ id: "enpassjson", name: "Enpass (json)" },
{ id: "safeincloudxml", name: "SafeInCloud (xml)" },
{ id: "pwsafexml", name: "Password Safe (xml)" },
{ id: "stickypasswordxml", name: "Sticky Password (xml)" },
{ id: "msecurecsv", name: "mSecure (csv)" },
{ id: "truekeycsv", name: "True Key (csv)" },
{ id: "passwordbossjson", name: "Password Boss (json)" },
{ id: "zohovaultcsv", name: "Zoho Vault (csv)" },
{ id: "splashidcsv", name: "SplashID (csv)" },
{ id: "passworddragonxml", name: "Password Dragon (xml)" },
{ id: "padlockcsv", name: "Padlock (csv)" },
{ id: "passboltcsv", name: "Passbolt (csv)" },
{ id: "clipperzhtml", name: "Clipperz (html)" },
{ id: "aviracsv", name: "Avira (csv)" },
{ id: "saferpasscsv", name: "SaferPass (csv)" },
{ id: "upmcsv", name: "Universal Password Manager (csv)" },
{ id: "ascendocsv", name: "Ascendo DataVault (csv)" },
{ id: "meldiumcsv", name: "Meldium (csv)" },
{ id: "passkeepcsv", name: "PassKeep (csv)" },
{ id: "operacsv", name: "Opera (csv)" },
{ id: "vivaldicsv", name: "Vivaldi (csv)" },
{ id: "gnomejson", name: "GNOME Passwords and Keys/Seahorse (json)" },
{ id: "blurcsv", name: "Blur (csv)" },
{ id: "passwordagentcsv", name: "Password Agent (csv)" },
{ id: "passpackcsv", name: "Passpack (csv)" },
{ id: "passmanjson", name: "Passman (json)" },
{ id: "avastcsv", name: "Avast Passwords (csv)" },
{ id: "avastjson", name: "Avast Passwords (json)" },
{ id: "fsecurefsk", name: "F-Secure KEY (fsk)" },
{ id: "kasperskytxt", name: "Kaspersky Password Manager (txt)" },
{ id: "remembearcsv", name: "RememBear (csv)" },
{ id: "passwordwallettxt", name: "PasswordWallet (txt)" },
{ id: "mykicsv", name: "Myki (csv)" },
{ id: "securesafecsv", name: "SecureSafe (csv)" },
{ id: "logmeoncecsv", name: "LogMeOnce (csv)" },
{ id: "blackberrycsv", name: "BlackBerry Password Keeper (csv)" },
{ id: "buttercupcsv", name: "Buttercup (csv)" },
{ id: "codebookcsv", name: "Codebook (csv)" },
{ id: "encryptrcsv", name: "Encryptr (csv)" },
{ id: "yoticsv", name: "Yoti (csv)" },
{ id: "nordpasscsv", name: "Nordpass (csv)" },
] as const;
export type ImportType =
| typeof featuredImportOptions[number]["id"]
| typeof regularImportOptions[number]["id"]
| "bitwardenpasswordprotected";
export class ImportService implements ImportServiceAbstraction {
featuredImportOptions = featuredImportOptions as readonly ImportOption[];
regularImportOptions = regularImportOptions as readonly ImportOption[];
constructor(
private cipherService: CipherService,
@@ -198,8 +208,12 @@ export class ImportService implements ImportServiceAbstraction {
}
}
getImporter(format: string, organizationId: string = null): Importer {
const importer = this.getImporterInstance(format);
getImporter(
format: ImportType,
organizationId: string = null,
password: string = null
): Importer {
const importer = this.getImporterInstance(format, password);
if (importer == null) {
return null;
}
@@ -207,8 +221,8 @@ export class ImportService implements ImportServiceAbstraction {
return importer;
}
private getImporterInstance(format: string) {
if (format == null || format === "") {
private getImporterInstance(format: ImportType, password: string) {
if (format == null) {
return null;
}
@@ -217,6 +231,13 @@ export class ImportService implements ImportServiceAbstraction {
return new BitwardenCsvImporter();
case "bitwardenjson":
return new BitwardenJsonImporter(this.cryptoService, this.i18nService);
case "bitwardenpasswordprotected":
return new BitwardenPasswordProtectedImporter(
this,
this.cryptoService,
this.i18nService,
password
);
case "lastpasscsv":
case "passboltcsv":
return new LastPassCsvImporter();
@@ -254,8 +275,8 @@ export class ImportService implements ImportServiceAbstraction {
return new OnePasswordMacCsvImporter();
case "keepercsv":
return new KeeperCsvImporter();
case "keeperjson":
return new KeeperJsonImporter();
// case "keeperjson":
// return new KeeperJsonImporter();
case "passworddragonxml":
return new PasswordDragonXmlImporter();
case "enpasscsv":