1
0
mirror of https://github.com/bitwarden/browser synced 2025-12-06 00:13:28 +00:00
Files
browser/libs/importer/src/services/import.service.ts
Bernd Schoolmann d92b2cbea2 [PM-11477] Remove deprecated cryptoservice functions (#10854)
* Remove deprecated cryptoservice functions

* Use getUserkeyWithLegacySupport to get userkey

* Fix tests

* Fix tests

* Fix tests

* Remove unused cryptoservice instances

* Fix build

* Remove unused apiService in constructor

* Fix encryption

* Ensure passed in key is used if present

* Fix sends and folders

* Fix tests

* Remove logged key

* Fix import for account restricted keys
2024-09-24 11:28:33 +02:00

511 lines
17 KiB
TypeScript

import { firstValueFrom, map } from "rxjs";
import { PinServiceAbstraction } from "@bitwarden/auth/common";
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
import { ImportCiphersRequest } from "@bitwarden/common/models/request/import-ciphers.request";
import { ImportOrganizationCiphersRequest } from "@bitwarden/common/models/request/import-organization-ciphers.request";
import { KvpRequest } from "@bitwarden/common/models/request/kvp.request";
import { ErrorResponse } from "@bitwarden/common/models/response/error.response";
import { CryptoService } from "@bitwarden/common/platform/abstractions/crypto.service";
import { EncryptService } from "@bitwarden/common/platform/abstractions/encrypt.service";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { Utils } from "@bitwarden/common/platform/misc/utils";
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
import { CollectionService } from "@bitwarden/common/vault/abstractions/collection.service";
import { FolderService } from "@bitwarden/common/vault/abstractions/folder/folder.service.abstraction";
import { CipherType } from "@bitwarden/common/vault/enums";
import { CipherRequest } from "@bitwarden/common/vault/models/request/cipher.request";
import { CollectionWithIdRequest } from "@bitwarden/common/vault/models/request/collection-with-id.request";
import { FolderWithIdRequest } from "@bitwarden/common/vault/models/request/folder-with-id.request";
import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
import { CollectionView } from "@bitwarden/common/vault/models/view/collection.view";
import { FolderView } from "@bitwarden/common/vault/models/view/folder.view";
import {
AscendoCsvImporter,
AvastCsvImporter,
AvastJsonImporter,
AviraCsvImporter,
BitwardenCsvImporter,
BitwardenPasswordProtectedImporter,
BlackBerryCsvImporter,
BlurCsvImporter,
ButtercupCsvImporter,
ChromeCsvImporter,
ClipperzHtmlImporter,
CodebookCsvImporter,
DashlaneCsvImporter,
DashlaneJsonImporter,
EncryptrCsvImporter,
EnpassCsvImporter,
EnpassJsonImporter,
FirefoxCsvImporter,
FSecureFskImporter,
GnomeJsonImporter,
KasperskyTxtImporter,
KeePass2XmlImporter,
KeePassXCsvImporter,
KeeperCsvImporter,
// KeeperJsonImporter,
LastPassCsvImporter,
LogMeOnceCsvImporter,
MSecureCsvImporter,
MeldiumCsvImporter,
MykiCsvImporter,
NordPassCsvImporter,
OnePassword1PifImporter,
OnePassword1PuxImporter,
OnePasswordMacCsvImporter,
OnePasswordWinCsvImporter,
PadlockCsvImporter,
PassKeepCsvImporter,
PasskyJsonImporter,
PassmanJsonImporter,
PasspackCsvImporter,
PasswordAgentCsvImporter,
PasswordBossJsonImporter,
PasswordDragonXmlImporter,
PasswordSafeXmlImporter,
PasswordWalletTxtImporter,
ProtonPassJsonImporter,
PsonoJsonImporter,
RememBearCsvImporter,
RoboFormCsvImporter,
SafariCsvImporter,
SafeInCloudXmlImporter,
SaferPassCsvImporter,
SecureSafeCsvImporter,
SplashIdCsvImporter,
StickyPasswordXmlImporter,
TrueKeyCsvImporter,
UpmCsvImporter,
YotiCsvImporter,
ZohoVaultCsvImporter,
} from "../importers";
import { Importer } from "../importers/importer";
import {
featuredImportOptions,
ImportOption,
ImportType,
regularImportOptions,
} from "../models/import-options";
import { ImportResult } from "../models/import-result";
import { ImportApiServiceAbstraction } from "../services/import-api.service.abstraction";
import { ImportServiceAbstraction } from "../services/import.service.abstraction";
export class ImportService implements ImportServiceAbstraction {
featuredImportOptions = featuredImportOptions as readonly ImportOption[];
regularImportOptions = regularImportOptions as readonly ImportOption[];
constructor(
private cipherService: CipherService,
private folderService: FolderService,
private importApiService: ImportApiServiceAbstraction,
private i18nService: I18nService,
private collectionService: CollectionService,
private cryptoService: CryptoService,
private encryptService: EncryptService,
private pinService: PinServiceAbstraction,
private accountService: AccountService,
) {}
getImportOptions(): ImportOption[] {
return this.featuredImportOptions.concat(this.regularImportOptions);
}
async import(
importer: Importer,
fileContents: string,
organizationId: string = null,
selectedImportTarget: FolderView | CollectionView = null,
canAccessImportExport: boolean,
): Promise<ImportResult> {
let importResult: ImportResult;
try {
importResult = await importer.parse(fileContents);
} catch (error) {
if (error instanceof SyntaxError) {
throw new Error(this.i18nService.t("importFormatError"));
}
throw error;
}
if (!importResult.success) {
if (!Utils.isNullOrWhitespace(importResult.errorMessage)) {
throw new Error(importResult.errorMessage);
}
throw new Error(this.i18nService.t("importFormatError"));
}
if (importResult.folders.length === 0 && importResult.ciphers.length === 0) {
throw new Error(this.i18nService.t("importNothingError"));
}
if (importResult.ciphers.length > 0) {
const halfway = Math.floor(importResult.ciphers.length / 2);
const last = importResult.ciphers.length - 1;
if (
this.badData(importResult.ciphers[0]) &&
this.badData(importResult.ciphers[halfway]) &&
this.badData(importResult.ciphers[last])
) {
throw new Error(this.i18nService.t("importFormatError"));
}
}
if (organizationId && !selectedImportTarget && !canAccessImportExport) {
const hasUnassignedCollections =
importResult.collectionRelationships.length < importResult.ciphers.length;
if (hasUnassignedCollections) {
throw new Error(this.i18nService.t("importUnassignedItemsError"));
}
}
try {
await this.setImportTarget(importResult, organizationId, selectedImportTarget);
if (organizationId != null) {
await this.handleOrganizationalImport(importResult, organizationId);
} else {
await this.handleIndividualImport(importResult);
}
} catch (error) {
const errorResponse = new ErrorResponse(error, 400);
throw this.handleServerError(errorResponse, importResult);
}
return importResult;
}
getImporter(
format: ImportType | "bitwardenpasswordprotected",
promptForPassword_callback: () => Promise<string>,
organizationId: string = null,
): Importer {
if (promptForPassword_callback == null) {
return null;
}
const importer = this.getImporterInstance(format, promptForPassword_callback);
if (importer == null) {
return null;
}
importer.organizationId = organizationId;
return importer;
}
private getImporterInstance(
format: ImportType | "bitwardenpasswordprotected",
promptForPassword_callback: () => Promise<string>,
) {
if (format == null) {
return null;
}
switch (format) {
case "bitwardencsv":
return new BitwardenCsvImporter();
case "bitwardenjson":
case "bitwardenpasswordprotected":
return new BitwardenPasswordProtectedImporter(
this.cryptoService,
this.encryptService,
this.i18nService,
this.cipherService,
this.pinService,
this.accountService,
promptForPassword_callback,
);
case "lastpasscsv":
case "passboltcsv":
return new LastPassCsvImporter();
case "keepassxcsv":
return new KeePassXCsvImporter();
case "aviracsv":
return new AviraCsvImporter();
case "blurcsv":
return new BlurCsvImporter();
case "safeincloudxml":
return new SafeInCloudXmlImporter();
case "padlockcsv":
return new PadlockCsvImporter();
case "keepass2xml":
return new KeePass2XmlImporter();
case "chromecsv":
case "operacsv":
case "vivaldicsv":
return new ChromeCsvImporter();
case "firefoxcsv":
return new FirefoxCsvImporter();
case "upmcsv":
return new UpmCsvImporter();
case "saferpasscsv":
return new SaferPassCsvImporter();
case "safaricsv":
return new SafariCsvImporter();
case "meldiumcsv":
return new MeldiumCsvImporter();
case "1password1pif":
return new OnePassword1PifImporter();
case "1password1pux":
return new OnePassword1PuxImporter();
case "1passwordwincsv":
return new OnePasswordWinCsvImporter();
case "1passwordmaccsv":
return new OnePasswordMacCsvImporter();
case "keepercsv":
return new KeeperCsvImporter();
// case "keeperjson":
// return new KeeperJsonImporter();
case "passworddragonxml":
return new PasswordDragonXmlImporter();
case "enpasscsv":
return new EnpassCsvImporter();
case "enpassjson":
return new EnpassJsonImporter();
case "pwsafexml":
return new PasswordSafeXmlImporter();
case "dashlanecsv":
return new DashlaneCsvImporter();
case "dashlanejson":
return new DashlaneJsonImporter();
case "msecurecsv":
return new MSecureCsvImporter();
case "stickypasswordxml":
return new StickyPasswordXmlImporter();
case "truekeycsv":
return new TrueKeyCsvImporter();
case "clipperzhtml":
return new ClipperzHtmlImporter();
case "roboformcsv":
return new RoboFormCsvImporter();
case "ascendocsv":
return new AscendoCsvImporter();
case "passwordbossjson":
return new PasswordBossJsonImporter();
case "zohovaultcsv":
return new ZohoVaultCsvImporter();
case "splashidcsv":
return new SplashIdCsvImporter();
case "passkeepcsv":
return new PassKeepCsvImporter();
case "gnomejson":
return new GnomeJsonImporter();
case "passwordagentcsv":
return new PasswordAgentCsvImporter();
case "passpackcsv":
return new PasspackCsvImporter();
case "passmanjson":
return new PassmanJsonImporter();
case "avastcsv":
return new AvastCsvImporter();
case "avastjson":
return new AvastJsonImporter();
case "fsecurefsk":
return new FSecureFskImporter();
case "kasperskytxt":
return new KasperskyTxtImporter();
case "remembearcsv":
return new RememBearCsvImporter();
case "passwordwallettxt":
return new PasswordWalletTxtImporter();
case "mykicsv":
return new MykiCsvImporter();
case "securesafecsv":
return new SecureSafeCsvImporter();
case "logmeoncecsv":
return new LogMeOnceCsvImporter();
case "blackberrycsv":
return new BlackBerryCsvImporter();
case "buttercupcsv":
return new ButtercupCsvImporter();
case "codebookcsv":
return new CodebookCsvImporter();
case "encryptrcsv":
return new EncryptrCsvImporter();
case "yoticsv":
return new YotiCsvImporter();
case "nordpasscsv":
return new NordPassCsvImporter();
case "psonojson":
return new PsonoJsonImporter();
case "passkyjson":
return new PasskyJsonImporter();
case "protonpass":
return new ProtonPassJsonImporter(this.i18nService);
default:
return null;
}
}
private async handleIndividualImport(importResult: ImportResult) {
const request = new ImportCiphersRequest();
const activeUserId = await firstValueFrom(
this.accountService.activeAccount$.pipe(map((a) => a?.id)),
);
for (let i = 0; i < importResult.ciphers.length; i++) {
const c = await this.cipherService.encrypt(importResult.ciphers[i], activeUserId);
request.ciphers.push(new CipherRequest(c));
}
const userKey = await this.cryptoService.getUserKeyWithLegacySupport(activeUserId);
if (importResult.folders != null) {
for (let i = 0; i < importResult.folders.length; i++) {
const f = await this.folderService.encrypt(importResult.folders[i], userKey);
request.folders.push(new FolderWithIdRequest(f));
}
}
if (importResult.folderRelationships != null) {
importResult.folderRelationships.forEach((r) =>
request.folderRelationships.push(new KvpRequest(r[0], r[1])),
);
}
return await this.importApiService.postImportCiphers(request);
}
private async handleOrganizationalImport(importResult: ImportResult, organizationId: string) {
const request = new ImportOrganizationCiphersRequest();
const activeUserId = await firstValueFrom(
this.accountService.activeAccount$.pipe(map((a) => a?.id)),
);
for (let i = 0; i < importResult.ciphers.length; i++) {
importResult.ciphers[i].organizationId = organizationId;
const c = await this.cipherService.encrypt(importResult.ciphers[i], activeUserId);
request.ciphers.push(new CipherRequest(c));
}
if (importResult.collections != null) {
for (let i = 0; i < importResult.collections.length; i++) {
importResult.collections[i].organizationId = organizationId;
const c = await this.collectionService.encrypt(importResult.collections[i]);
request.collections.push(new CollectionWithIdRequest(c));
}
}
if (importResult.collectionRelationships != null) {
importResult.collectionRelationships.forEach((r) =>
request.collectionRelationships.push(new KvpRequest(r[0], r[1])),
);
}
return await this.importApiService.postImportOrganizationCiphers(organizationId, request);
}
private badData(c: CipherView) {
return (
(c.name == null || c.name === "--") &&
c.type === CipherType.Login &&
c.login != null &&
Utils.isNullOrWhitespace(c.login.password)
);
}
private handleServerError(errorResponse: ErrorResponse, importResult: ImportResult): Error {
if (errorResponse.validationErrors == null) {
return new Error(errorResponse.message);
}
let errorMessage = "";
Object.entries(errorResponse.validationErrors).forEach(([key, value], index) => {
let item;
let itemType;
const i = Number(key.match(/[0-9]+/)[0]);
switch (key.match(/^\w+/)[0]) {
case "Ciphers":
item = importResult.ciphers[i];
itemType = CipherType[item.type];
break;
case "Folders":
item = importResult.folders[i];
itemType = "Folder";
break;
case "Collections":
item = importResult.collections[i];
itemType = "Collection";
break;
default:
return;
}
if (index > 0) {
errorMessage += "\n\n";
}
if (itemType !== "Folder" && itemType !== "Collection") {
errorMessage += "[" + (i + 1) + "] ";
}
errorMessage += "[" + itemType + '] "' + item.name + '": ' + value;
});
return new Error(errorMessage);
}
private async setImportTarget(
importResult: ImportResult,
organizationId: string,
importTarget: FolderView | CollectionView,
) {
if (!importTarget) {
return;
}
if (organizationId) {
if (!(importTarget instanceof CollectionView)) {
throw new Error(this.i18nService.t("errorAssigningTargetCollection"));
}
const noCollectionRelationShips: [number, number][] = [];
importResult.ciphers.forEach((c, index) => {
if (
!Array.isArray(importResult.collectionRelationships) ||
!importResult.collectionRelationships.some(([cipherPos]) => cipherPos === index)
) {
noCollectionRelationShips.push([index, 0]);
}
});
const collections: CollectionView[] = [...importResult.collections];
importResult.collections = [importTarget as CollectionView];
collections.map((x) => {
const f = new CollectionView();
f.name = `${importTarget.name}/${x.name}`;
importResult.collections.push(f);
});
const relationships: [number, number][] = [...importResult.collectionRelationships];
importResult.collectionRelationships = [...noCollectionRelationShips];
relationships.map((x) => {
importResult.collectionRelationships.push([x[0], x[1] + 1]);
});
return;
}
if (!(importTarget instanceof FolderView)) {
throw new Error(this.i18nService.t("errorAssigningTargetFolder"));
}
const noFolderRelationShips: [number, number][] = [];
importResult.ciphers.forEach((c, index) => {
if (Utils.isNullOrEmpty(c.folderId)) {
c.folderId = importTarget.id;
noFolderRelationShips.push([index, 0]);
}
});
const folders: FolderView[] = [...importResult.folders];
importResult.folders = [importTarget as FolderView];
folders.map((x) => {
const newFolderName = `${importTarget.name}/${x.name}`;
const f = new FolderView();
f.name = newFolderName;
importResult.folders.push(f);
});
const relationships: [number, number][] = [...importResult.folderRelationships];
importResult.folderRelationships = [...noFolderRelationShips];
relationships.map((x) => {
importResult.folderRelationships.push([x[0], x[1] + 1]);
});
}
}