1
0
mirror of https://github.com/bitwarden/browser synced 2025-12-15 07:43:35 +00:00

Dashlane Csv-Importer (#708)

* Move existing dashlane importer into dashlaneImporters

* Add testData for Dashlane CSV importer

* Add dashlane Csv importer and unit tests

* Fixed linting issues

* Moved dashlaneCsv types to own file

* Register DashlaneCsv importer

* Removed temp private method and use base impl

* rename spec imports

* Move scope of mapped columns

* Migrate folders into collection if imported via org
This commit is contained in:
Daniel James Smith
2022-03-07 19:21:23 +01:00
committed by GitHub
parent 6aae3beb76
commit 6e345bc4cc
13 changed files with 749 additions and 11 deletions

View File

@@ -7,6 +7,7 @@ export const featuredImportOptions = [
{ id: "bitwardenjson", name: "Bitwarden (json)" },
{ id: "bitwardencsv", name: "Bitwarden (csv)" },
{ id: "chromecsv", name: "Chrome (csv)" },
{ id: "dashlanecsv", name: "Dashlane (csv)" },
{ id: "dashlanejson", name: "Dashlane (json)" },
{ id: "firefoxcsv", name: "Firefox (csv)" },
{ id: "keepass2xml", name: "KeePass 2 (xml)" },

View File

@@ -0,0 +1,271 @@
import { CipherType } from "../../enums/cipherType";
import { SecureNoteType } from "../../enums/secureNoteType";
import { ImportResult } from "../../models/domain/importResult";
import { CardView } from "../../models/view/cardView";
import { CipherView } from "../../models/view/cipherView";
import { IdentityView } from "../../models/view/identityView";
import { LoginView } from "../../models/view/loginView";
import { BaseImporter } from "../baseImporter";
import { Importer } from "../importer";
import {
CredentialsRecord,
IdRecord,
PaymentsRecord,
PersonalInformationRecord,
SecureNoteRecord,
} from "./types/dashlaneCsvTypes";
const _mappedCredentialsColums = new Set([
"title",
"note",
"username",
"password",
"url",
"otpSecret",
"category",
]);
const _mappedPersonalInfoAsIdentiyColumns = new Set([
"type",
"title",
"first_name",
"middle_name",
"last_name",
"login",
"email",
"phone_number",
"address",
"country",
"state",
"city",
"zip",
// Skip item_name as we already have set a combined name
"item_name",
]);
const _mappedSecureNoteColumns = new Set(["title", "note"]);
export class DashlaneCsvImporter extends BaseImporter implements Importer {
parse(data: string): Promise<ImportResult> {
const result = new ImportResult();
const results = this.parseCsv(data, true);
if (results == null) {
result.success = false;
return Promise.resolve(result);
}
if (results[0].type != null && results[0].title != null) {
const personalRecords = results as PersonalInformationRecord[];
// If personalRecords has only one "name" then create an Identity-Cipher
if (personalRecords.filter((x) => x.type === "name").length === 1) {
const cipher = this.initLoginCipher();
cipher.type = CipherType.Identity;
cipher.identity = new IdentityView();
results.forEach((row) => {
this.parsePersonalInformationRecordAsIdentity(cipher, row);
});
this.cleanupCipher(cipher);
result.ciphers.push(cipher);
result.success = true;
return Promise.resolve(result);
}
}
results.forEach((row) => {
const cipher = this.initLoginCipher();
const rowKeys = Object.keys(row);
if (rowKeys[0] === "username") {
this.processFolder(result, row.category);
this.parseCredentialsRecord(cipher, row);
}
if (rowKeys[0] === "type" && rowKeys[1] === "account_name") {
this.parsePaymentRecord(cipher, row);
}
if (rowKeys[0] === "type" && rowKeys[1] === "number") {
this.parseIdRecord(cipher, row);
}
if ((rowKeys[0] === "type") != null && rowKeys[1] === "title") {
this.parsePersonalInformationRecord(cipher, row);
}
if (rowKeys[0] === "title" && rowKeys[1] === "note") {
this.parseSecureNoteRecords(cipher, row);
}
this.convertToNoteIfNeeded(cipher);
this.cleanupCipher(cipher);
result.ciphers.push(cipher);
});
if (this.organization) {
this.moveFoldersToCollections(result);
}
result.success = true;
return Promise.resolve(result);
}
parseCredentialsRecord(cipher: CipherView, row: CredentialsRecord) {
cipher.type = CipherType.Login;
cipher.login = new LoginView();
cipher.name = row.title;
cipher.notes = row.note;
cipher.login.username = row.username;
cipher.login.password = row.password;
cipher.login.totp = row.otpSecret;
cipher.login.uris = this.makeUriArray(row.url);
this.importUnmappedFields(cipher, row, _mappedCredentialsColums);
}
parsePaymentRecord(cipher: CipherView, row: PaymentsRecord) {
cipher.type = CipherType.Card;
cipher.card = new CardView();
cipher.name = row.account_name;
let mappedValues: string[] = [];
switch (row.type) {
case "credit_card":
cipher.card.cardholderName = row.account_name;
cipher.card.number = row.cc_number;
cipher.card.brand = this.getCardBrand(cipher.card.number);
cipher.card.code = row.code;
cipher.card.expMonth = row.expiration_month;
cipher.card.expYear = row.expiration_year.substring(2, 4);
// If you add more mapped fields please extend this
mappedValues = [
"account_name",
"account_holder",
"cc_number",
"code",
"expiration_month",
"expiration_year",
];
break;
case "bank":
cipher.card.cardholderName = row.account_holder;
cipher.card.number = row.account_number;
// If you add more mapped fields please extend this
mappedValues = ["account_name", "account_holder", "account_number"];
break;
default:
break;
}
this.importUnmappedFields(cipher, row, new Set(mappedValues));
}
parseIdRecord(cipher: CipherView, row: IdRecord) {
cipher.type = CipherType.Identity;
cipher.identity = new IdentityView();
const mappedValues: string[] = ["name", "number"];
switch (row.type) {
case "card":
cipher.name = `${row.name} ${row.type}`;
this.processFullName(cipher, row.name);
cipher.identity.licenseNumber = row.number;
break;
case "passport":
cipher.name = `${row.name} ${row.type}`;
this.processFullName(cipher, row.name);
cipher.identity.passportNumber = row.number;
break;
case "license":
cipher.name = `${row.name} ${row.type}`;
this.processFullName(cipher, row.name);
cipher.identity.licenseNumber = row.number;
cipher.identity.state = row.state;
mappedValues.push("state");
break;
case "social_security":
cipher.name = `${row.name} ${row.type}`;
this.processFullName(cipher, row.name);
cipher.identity.ssn = row.number;
break;
case "tax_number":
cipher.name = row.type;
cipher.identity.licenseNumber = row.number;
break;
default:
break;
}
// If you add more mapped fields please extend this
this.importUnmappedFields(cipher, row, new Set(mappedValues));
}
parsePersonalInformationRecord(cipher: CipherView, row: PersonalInformationRecord) {
cipher.type = CipherType.SecureNote;
cipher.secureNote.type = SecureNoteType.Generic;
if (row.type === "name") {
cipher.name = `${row.title} ${row.first_name} ${row.middle_name} ${row.last_name}`
.replace(" ", " ")
.trim();
} else {
cipher.name = row.item_name;
}
const dataRow = row as any;
Object.keys(row).forEach((key) => {
this.processKvp(cipher, key, dataRow[key]);
});
}
parsePersonalInformationRecordAsIdentity(cipher: CipherView, row: PersonalInformationRecord) {
switch (row.type) {
case "name":
this.processFullName(cipher, `${row.first_name} ${row.middle_name} ${row.last_name}`);
cipher.identity.title = row.title;
cipher.name = cipher.identity.fullName;
cipher.identity.username = row.login;
break;
case "email":
cipher.identity.email = row.email;
break;
case "number":
cipher.identity.phone = row.phone_number;
break;
case "address":
cipher.identity.address1 = row.address;
cipher.identity.city = row.city;
cipher.identity.postalCode = row.zip;
cipher.identity.state = row.state;
cipher.identity.country = row.country;
break;
default:
break;
}
this.importUnmappedFields(cipher, row, _mappedPersonalInfoAsIdentiyColumns);
}
parseSecureNoteRecords(cipher: CipherView, row: SecureNoteRecord) {
cipher.type = CipherType.SecureNote;
cipher.secureNote.type = SecureNoteType.Generic;
cipher.name = row.title;
cipher.notes = row.note;
this.importUnmappedFields(cipher, row, _mappedSecureNoteColumns);
}
importUnmappedFields(cipher: CipherView, row: any, mappedValues: Set<string>) {
const unmappedFields = Object.keys(row).filter((x) => !mappedValues.has(x));
unmappedFields.forEach((key) => {
const item = row as any;
this.processKvp(cipher, key, item[key]);
});
}
}

View File

@@ -1,13 +1,12 @@
import { CipherType } from "../enums/cipherType";
import { SecureNoteType } from "../enums/secureNoteType";
import { ImportResult } from "../models/domain/importResult";
import { CardView } from "../models/view/cardView";
import { CipherView } from "../models/view/cipherView";
import { IdentityView } from "../models/view/identityView";
import { SecureNoteView } from "../models/view/secureNoteView";
import { BaseImporter } from "./baseImporter";
import { Importer } from "./importer";
import { CipherType } from "../../enums/cipherType";
import { SecureNoteType } from "../../enums/secureNoteType";
import { ImportResult } from "../../models/domain/importResult";
import { CardView } from "../../models/view/cardView";
import { CipherView } from "../../models/view/cipherView";
import { IdentityView } from "../../models/view/identityView";
import { SecureNoteView } from "../../models/view/secureNoteView";
import { BaseImporter } from "../baseImporter";
import { Importer } from "../importer";
const HandledResults = new Set([
"ADDRESS",

View File

@@ -0,0 +1,68 @@
// tslint:disable
export class CredentialsRecord {
username: string;
username2: string;
username3: string;
title: string;
password: string;
note: string;
url: string;
category: string;
otpSecret: string;
}
export class PaymentsRecord {
type: string;
account_name: string;
account_holder: string;
cc_number: string;
code: string;
expiration_month: string;
expiration_year: string;
routing_number: string;
account_number: string;
country: string;
issuing_bank: string;
}
export class IdRecord {
type: string;
number: string;
name: string;
issue_date: string;
expiration_date: string;
place_of_issue: string;
state: string;
}
export class PersonalInformationRecord {
type: string;
title: string;
first_name: string;
middle_name: string;
last_name: string;
login: string;
date_of_birth: string;
place_of_birth: string;
email: string;
email_type: string;
item_name: string;
phone_number: string;
address: string;
country: string;
state: string;
city: string;
zip: string;
address_recipient: string;
address_building: string;
address_apartment: string;
address_floor: string;
address_door_code: string;
job_title: string;
url: string;
}
export class SecureNoteRecord {
title: string;
note: string;
}

View File

@@ -26,7 +26,8 @@ import { ButtercupCsvImporter } from "../importers/buttercupCsvImporter";
import { ChromeCsvImporter } from "../importers/chromeCsvImporter";
import { ClipperzHtmlImporter } from "../importers/clipperzHtmlImporter";
import { CodebookCsvImporter } from "../importers/codebookCsvImporter";
import { DashlaneJsonImporter } from "../importers/dashlaneJsonImporter";
import { DashlaneCsvImporter } from "../importers/dashlaneImporters/dashlaneCsvImporter";
import { DashlaneJsonImporter } from "../importers/dashlaneImporters/dashlaneJsonImporter";
import { EncryptrCsvImporter } from "../importers/encryptrCsvImporter";
import { EnpassCsvImporter } from "../importers/enpassCsvImporter";
import { EnpassJsonImporter } from "../importers/enpassJsonImporter";
@@ -218,6 +219,8 @@ export class ImportService implements ImportServiceAbstraction {
return new EnpassJsonImporter();
case "pwsafexml":
return new PasswordSafeXmlImporter();
case "dashlanecsv":
return new DashlaneCsvImporter();
case "dashlanejson":
return new DashlaneJsonImporter();
case "msecurecsv":