1
0
mirror of https://github.com/bitwarden/browser.git synced 2025-02-25 02:51:59 +01:00

[PM-11882] Handled identity item and unsupported items during ProtonPass import. ()

This commit is contained in:
Aftab Ali 2024-09-18 21:58:47 +05:30 committed by GitHub
parent 2d7fb035d4
commit 0f3d8a6f89
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
6 changed files with 504 additions and 6 deletions

View File

@ -85,7 +85,7 @@ describe("Protonpass Json Importer", () => {
// "My Secure Note" is assigned to folder "Personal"
expect(result.folderRelationships[1]).toEqual([1, 0]);
// "Other vault login" is assigned to folder "Test"
expect(result.folderRelationships[3]).toEqual([3, 1]);
expect(result.folderRelationships[4]).toEqual([4, 1]);
});
it("should create collections if part of an organization", async () => {
@ -102,7 +102,7 @@ describe("Protonpass Json Importer", () => {
// "My Secure Note" is assigned to folder "Personal"
expect(result.collectionRelationships[1]).toEqual([1, 0]);
// "Other vault login" is assigned to folder "Test"
expect(result.collectionRelationships[3]).toEqual([3, 1]);
expect(result.collectionRelationships[4]).toEqual([4, 1]);
});
it("should not add deleted items", async () => {
@ -114,7 +114,7 @@ describe("Protonpass Json Importer", () => {
expect(cipher.name).not.toBe("My Deleted Note");
}
expect(ciphers.length).toBe(4);
expect(ciphers.length).toBe(5);
});
it("should set favorites", async () => {
@ -126,4 +126,97 @@ describe("Protonpass Json Importer", () => {
expect(ciphers[1].favorite).toBe(false);
expect(ciphers[2].favorite).toBe(true);
});
it("should skip unsupported items", async () => {
const testDataJson = JSON.stringify(testData);
const result = await importer.parse(testDataJson);
expect(result != null).toBe(true);
const ciphers = result.ciphers;
expect(ciphers.length).toBe(5);
expect(ciphers[4].type).toEqual(CipherType.Login);
});
it("should parse identity data", async () => {
const testDataJson = JSON.stringify(testData);
const result = await importer.parse(testDataJson);
expect(result != null).toBe(true);
result.ciphers.shift();
result.ciphers.shift();
result.ciphers.shift();
const cipher = result.ciphers.shift();
expect(cipher.type).toEqual(CipherType.Identity);
expect(cipher.identity.firstName).toBe("Test");
expect(cipher.identity.middleName).toBe("1");
expect(cipher.identity.lastName).toBe("1");
expect(cipher.identity.email).toBe("test@gmail.com");
expect(cipher.identity.phone).toBe("7507951789");
expect(cipher.identity.company).toBe("Bitwarden");
expect(cipher.identity.ssn).toBe("98378264782");
expect(cipher.identity.passportNumber).toBe("7173716378612");
expect(cipher.identity.licenseNumber).toBe("21234");
expect(cipher.identity.address1).toBe("Bitwarden");
expect(cipher.identity.address2).toBe("23 Street");
expect(cipher.identity.address3).toBe("12th Foor Test County");
expect(cipher.identity.city).toBe("New York");
expect(cipher.identity.state).toBe("Test");
expect(cipher.identity.postalCode).toBe("4038456");
expect(cipher.identity.country).toBe("US");
expect(cipher.fields.length).toEqual(13);
expect(cipher.fields.at(0).name).toEqual("gender");
expect(cipher.fields.at(0).value).toEqual("Male");
expect(cipher.fields.at(0).type).toEqual(FieldType.Text);
expect(cipher.fields.at(1).name).toEqual("TestPersonal");
expect(cipher.fields.at(1).value).toEqual("Personal");
expect(cipher.fields.at(1).type).toEqual(FieldType.Text);
expect(cipher.fields.at(2).name).toEqual("TestAddress");
expect(cipher.fields.at(2).value).toEqual("Address");
expect(cipher.fields.at(2).type).toEqual(FieldType.Text);
expect(cipher.fields.at(3).name).toEqual("xHandle");
expect(cipher.fields.at(3).value).toEqual("@twiter");
expect(cipher.fields.at(3).type).toEqual(FieldType.Text);
expect(cipher.fields.at(4).name).toEqual("secondPhoneNumber");
expect(cipher.fields.at(4).value).toEqual("243538978");
expect(cipher.fields.at(4).type).toEqual(FieldType.Text);
expect(cipher.fields.at(5).name).toEqual("instagram");
expect(cipher.fields.at(5).value).toEqual("@insta");
expect(cipher.fields.at(5).type).toEqual(FieldType.Text);
expect(cipher.fields.at(6).name).toEqual("TestContact");
expect(cipher.fields.at(6).value).toEqual("Contact");
expect(cipher.fields.at(6).type).toEqual(FieldType.Hidden);
expect(cipher.fields.at(7).name).toEqual("jobTitle");
expect(cipher.fields.at(7).value).toEqual("Engineer");
expect(cipher.fields.at(7).type).toEqual(FieldType.Text);
expect(cipher.fields.at(8).name).toEqual("workPhoneNumber");
expect(cipher.fields.at(8).value).toEqual("78236476238746");
expect(cipher.fields.at(8).type).toEqual(FieldType.Text);
expect(cipher.fields.at(9).name).toEqual("TestWork");
expect(cipher.fields.at(9).value).toEqual("Work");
expect(cipher.fields.at(9).type).toEqual(FieldType.Hidden);
expect(cipher.fields.at(10).name).toEqual("TestSection");
expect(cipher.fields.at(10).value).toEqual("Section");
expect(cipher.fields.at(10).type).toEqual(FieldType.Text);
expect(cipher.fields.at(11).name).toEqual("TestSectionHidden");
expect(cipher.fields.at(11).value).toEqual("SectionHidden");
expect(cipher.fields.at(11).type).toEqual(FieldType.Hidden);
expect(cipher.fields.at(12).name).toEqual("TestExtra");
expect(cipher.fields.at(12).value).toEqual("Extra");
expect(cipher.fields.at(12).type).toEqual(FieldType.Text);
});
});

View File

@ -138,6 +138,144 @@ export const testData: ProtonPassJsonFile = {
modifyTime: 1689182908,
pinned: false,
},
{
itemId:
"gliCOyyJOsoBf5QIijvCF4QsPij3q_MR4nCXZ2sXm7YCJCfHjrRD_p2XG9vLsaytErsQvMhcLISVS7q8-7SCkg==",
shareId:
"TpawpLbs1nuUlQUCtgKZgb3zgAvbrGrOaqOylKqVe_RLROEyUvMq8_ZEuGw73PGRUSr89iNtQ2NosuggP54nwA==",
data: {
metadata: {
name: "Identity",
note: "",
itemUuid: "c2e52768",
},
extraFields: [
{
fieldName: "TestExtra",
type: "text",
data: {
content: "Extra",
},
},
],
type: "identity",
content: {
fullName: "Test 1",
email: "test@gmail.com",
phoneNumber: "7507951789",
firstName: "Test",
middleName: "1",
lastName: "Test",
birthdate: "",
gender: "Male",
extraPersonalDetails: [
{
fieldName: "TestPersonal",
type: "text",
data: {
content: "Personal",
},
},
],
organization: "Bitwarden",
streetAddress: "23 Street",
zipOrPostalCode: "4038456",
city: "New York",
stateOrProvince: "Test",
countryOrRegion: "US",
floor: "12th Foor",
county: "Test County",
extraAddressDetails: [
{
fieldName: "TestAddress",
type: "text",
data: {
content: "Address",
},
},
],
socialSecurityNumber: "98378264782",
passportNumber: "7173716378612",
licenseNumber: "21234",
website: "",
xHandle: "@twiter",
secondPhoneNumber: "243538978",
linkedin: "",
reddit: "",
facebook: "",
yahoo: "",
instagram: "@insta",
extraContactDetails: [
{
fieldName: "TestContact",
type: "hidden",
data: {
content: "Contact",
},
},
],
company: "Bitwarden",
jobTitle: "Engineer",
personalWebsite: "",
workPhoneNumber: "78236476238746",
workEmail: "",
extraWorkDetails: [
{
fieldName: "TestWork",
type: "hidden",
data: {
content: "Work",
},
},
],
extraSections: [
{
sectionName: "TestSection",
sectionFields: [
{
fieldName: "TestSection",
type: "text",
data: {
content: "Section",
},
},
{
fieldName: "TestSectionHidden",
type: "hidden",
data: {
content: "SectionHidden",
},
},
],
},
],
},
},
state: 1,
aliasEmail: null,
contentFormatVersion: 6,
createTime: 1725707298,
modifyTime: 1725707298,
pinned: false,
},
{
itemId:
"WTKLZtKfHIC3Gv7gRXUANifNjj0gN3P_52I4MznAzig9GSb_OgJ0qcZ8taOZyfsFTLOWBslXwI-HSMWXVmnKzQ==",
shareId:
"TpawpLbs1nuUlQUCtgKZgb3zgAvbrGrOaqOylKqVe_RLROEyUvMq8_ZEuGw73PGRUSr89iNtQ2NosuggP54nwA==",
data: {
metadata: { name: "Alias", note: "", itemUuid: "576f14fa" },
extraFields: [],
type: "alias",
content: {},
},
state: 1,
aliasEmail: "alias.removing005@passinbox.com",
contentFormatVersion: 6,
createTime: 1725708208,
modifyTime: 1725708208,
pinned: false,
},
],
},
REDACTED_VAULT_ID_B: {

View File

@ -0,0 +1,66 @@
import { processNames } from "./protonpass-import-utils";
describe("processNames", () => {
it("should use only fullName to map names if it contains at least three words, ignoring individual name fields", () => {
const result = processNames("Alice Beth Carter", "Kevin", "", "");
expect(result).toEqual({
mappedFirstName: "Alice",
mappedMiddleName: "Beth",
mappedLastName: "Carter",
});
});
it("should map extra words to the middle name if fullName contains more than three words", () => {
const result = processNames("Alice Beth Middle Carter", "", "", "");
expect(result).toEqual({
mappedFirstName: "Alice",
mappedMiddleName: "Beth Middle",
mappedLastName: "Carter",
});
});
it("should map names correctly even if fullName has words separated by more than one space", () => {
const result = processNames("Alice Carter", "", "", "");
expect(result).toEqual({
mappedFirstName: "Alice",
mappedMiddleName: "",
mappedLastName: "Carter",
});
});
it("should handle a single name in fullName and use middleName and lastName to populate rest of names", () => {
const result = processNames("Alice", "", "Beth", "Carter");
expect(result).toEqual({
mappedFirstName: "Alice",
mappedMiddleName: "Beth",
mappedLastName: "Carter",
});
});
it("should correctly map fullName when it only contains two words", () => {
const result = processNames("Alice Carter", "", "", "");
expect(result).toEqual({
mappedFirstName: "Alice",
mappedMiddleName: "",
mappedLastName: "Carter",
});
});
it("should map middle name from middleName if fullName only contains two words", () => {
const result = processNames("Alice Carter", "", "Beth", "");
expect(result).toEqual({
mappedFirstName: "Alice",
mappedMiddleName: "Beth",
mappedLastName: "Carter",
});
});
it("should fall back to firstName, middleName, and lastName if fullName is empty", () => {
const result = processNames("", "Alice", "Beth", "Carter");
expect(result).toEqual({
mappedFirstName: "Alice",
mappedMiddleName: "Beth",
mappedLastName: "Carter",
});
});
});

View File

@ -0,0 +1,21 @@
export function processNames(
fullname: string | null,
firstname: string | null,
middlename: string | null,
lastname: string | null,
) {
let mappedFirstName = firstname;
let mappedMiddleName = middlename;
let mappedLastName = lastname;
if (fullname) {
const parts = fullname.trim().split(/\s+/);
// Assign parts to first, middle, and last name based on the number of parts
mappedFirstName = parts[0] || firstname;
mappedLastName = parts.length > 1 ? parts[parts.length - 1] : lastname;
mappedMiddleName = parts.length > 2 ? parts.slice(1, -1).join(" ") : middlename;
}
return { mappedFirstName, mappedMiddleName, mappedLastName };
}

View File

@ -1,24 +1,110 @@
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { FieldType, SecureNoteType, CipherType } from "@bitwarden/common/vault/enums";
import { CardView } from "@bitwarden/common/vault/models/view/card.view";
import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
import { IdentityView } from "@bitwarden/common/vault/models/view/identity.view";
import { SecureNoteView } from "@bitwarden/common/vault/models/view/secure-note.view";
import { ImportResult } from "../../models/import-result";
import { BaseImporter } from "../base-importer";
import { Importer } from "../importer";
import { processNames } from "./protonpass-import-utils";
import {
ProtonPassCreditCardItemContent,
ProtonPassIdentityItemContent,
ProtonPassIdentityItemExtraSection,
ProtonPassItemExtraField,
ProtonPassItemState,
ProtonPassJsonFile,
ProtonPassLoginItemContent,
} from "./types/protonpass-json-type";
export class ProtonPassJsonImporter extends BaseImporter implements Importer {
private mappedIdentityItemKeys = [
"fullName",
"firstName",
"middleName",
"lastName",
"email",
"phoneNumber",
"company",
"socialSecurityNumber",
"passportNumber",
"licenseNumber",
"organization",
"streetAddress",
"floor",
"county",
"city",
"stateOrProvince",
"zipOrPostalCode",
"countryOrRegion",
];
private identityItemExtraFieldsKeys = [
"extraPersonalDetails",
"extraAddressDetails",
"extraContactDetails",
"extraWorkDetails",
"extraSections",
];
constructor(private i18nService: I18nService) {
super();
}
private processIdentityItemUnmappedAndExtraFields(
cipher: CipherView,
identityItem: ProtonPassIdentityItemContent,
) {
Object.keys(identityItem).forEach((key) => {
if (
!this.mappedIdentityItemKeys.includes(key) &&
!this.identityItemExtraFieldsKeys.includes(key)
) {
this.processKvp(
cipher,
key,
identityItem[key as keyof ProtonPassIdentityItemContent] as string,
);
return;
}
if (this.identityItemExtraFieldsKeys.includes(key)) {
if (key !== "extraSections") {
const extraFields = identityItem[
key as keyof ProtonPassIdentityItemContent
] as ProtonPassItemExtraField[];
extraFields?.forEach((extraField) => {
this.processKvp(
cipher,
extraField.fieldName,
extraField.data.content,
extraField.type === "hidden" ? FieldType.Hidden : FieldType.Text,
);
});
} else {
const extraSections = identityItem[
key as keyof ProtonPassIdentityItemContent
] as ProtonPassIdentityItemExtraSection[];
extraSections?.forEach((extraSection) => {
extraSection.sectionFields?.forEach((extraField) => {
this.processKvp(
cipher,
extraField.fieldName,
extraField.data.content,
extraField.type === "hidden" ? FieldType.Hidden : FieldType.Text,
);
});
});
}
}
});
}
parse(data: string): Promise<ImportResult> {
const result = new ImportResult();
const results: ProtonPassJsonFile = JSON.parse(data);
@ -38,7 +124,6 @@ export class ProtonPassJsonImporter extends BaseImporter implements Importer {
if (item.state == ProtonPassItemState.TRASHED) {
continue;
}
this.processFolder(result, vault.name);
const cipher = this.initLoginCipher();
cipher.name = this.getValueOrDefault(item.data.metadata.name, "--");
@ -96,8 +181,55 @@ export class ProtonPassJsonImporter extends BaseImporter implements Importer {
break;
}
case "identity": {
const identityContent = item.data.content as ProtonPassIdentityItemContent;
cipher.type = CipherType.Identity;
cipher.identity = new IdentityView();
const { mappedFirstName, mappedMiddleName, mappedLastName } = processNames(
this.getValueOrDefault(identityContent.fullName),
this.getValueOrDefault(identityContent.firstName),
this.getValueOrDefault(identityContent.middleName),
this.getValueOrDefault(identityContent.lastName),
);
cipher.identity.firstName = mappedFirstName;
cipher.identity.middleName = mappedMiddleName;
cipher.identity.lastName = mappedLastName;
cipher.identity.email = this.getValueOrDefault(identityContent.email);
cipher.identity.phone = this.getValueOrDefault(identityContent.phoneNumber);
cipher.identity.company = this.getValueOrDefault(identityContent.company);
cipher.identity.ssn = this.getValueOrDefault(identityContent.socialSecurityNumber);
cipher.identity.passportNumber = this.getValueOrDefault(identityContent.passportNumber);
cipher.identity.licenseNumber = this.getValueOrDefault(identityContent.licenseNumber);
const address3 =
`${identityContent.floor ?? ""} ${identityContent.county ?? ""}`.trim();
cipher.identity.address1 = this.getValueOrDefault(identityContent.organization);
cipher.identity.address2 = this.getValueOrDefault(identityContent.streetAddress);
cipher.identity.address3 = this.getValueOrDefault(address3);
cipher.identity.city = this.getValueOrDefault(identityContent.city);
cipher.identity.state = this.getValueOrDefault(identityContent.stateOrProvince);
cipher.identity.postalCode = this.getValueOrDefault(identityContent.zipOrPostalCode);
cipher.identity.country = this.getValueOrDefault(identityContent.countryOrRegion);
this.processIdentityItemUnmappedAndExtraFields(cipher, identityContent);
for (const extraField of item.data.extraFields) {
this.processKvp(
cipher,
extraField.fieldName,
extraField.data.content,
extraField.type === "hidden" ? FieldType.Hidden : FieldType.Text,
);
}
break;
}
default:
continue;
}
this.processFolder(result, vault.name);
this.cleanupCipher(cipher);
result.ciphers.push(cipher);
}

View File

@ -36,8 +36,11 @@ export type ProtonPassItemData = {
metadata: ProtonPassItemMetadata;
extraFields: ProtonPassItemExtraField[];
platformSpecific?: any;
type: "login" | "alias" | "creditCard" | "note";
content: ProtonPassLoginItemContent | ProtonPassCreditCardItemContent;
type: "login" | "alias" | "creditCard" | "note" | "identity";
content:
| ProtonPassLoginItemContent
| ProtonPassCreditCardItemContent
| ProtonPassIdentityItemContent;
};
export type ProtonPassItemMetadata = {
@ -74,3 +77,48 @@ export type ProtonPassCreditCardItemContent = {
expirationDate?: string;
pin?: string;
};
export type ProtonPassIdentityItemExtraSection = {
sectionName?: string;
sectionFields?: ProtonPassItemExtraField[];
};
export type ProtonPassIdentityItemContent = {
fullName?: string;
email?: string;
phoneNumber?: string;
firstName?: string;
middleName?: string;
lastName?: string;
birthdate?: string;
gender?: string;
extraPersonalDetails?: ProtonPassItemExtraField[];
organization?: string;
streetAddress?: string;
zipOrPostalCode?: string;
city?: string;
stateOrProvince?: string;
countryOrRegion?: string;
floor?: string;
county?: string;
extraAddressDetails?: ProtonPassItemExtraField[];
socialSecurityNumber?: string;
passportNumber?: string;
licenseNumber?: string;
website?: string;
xHandle?: string;
secondPhoneNumber?: string;
linkedin?: string;
reddit?: string;
facebook?: string;
yahoo?: string;
instagram?: string;
extraContactDetails?: ProtonPassItemExtraField[];
company?: string;
jobTitle?: string;
personalWebsite?: string;
workPhoneNumber?: string;
workEmail?: string;
extraWorkDetails?: ProtonPassItemExtraField[];
extraSections?: ProtonPassIdentityItemExtraSection[];
};