From 840fe4583ef0a7f9b8e4ee424266722fa6e5b65e Mon Sep 17 00:00:00 2001 From: Aftab Ali Date: Tue, 10 Sep 2024 00:34:35 +0530 Subject: [PATCH 1/3] Handled identity item and unsupported items during ProtonPass import. --- .../spec/protonpass-json-importer.spec.ts | 91 ++++++++++ .../protonpass-identity.json.ts | 155 ++++++++++++++++++ .../protonpass/protonpass-json-importer.ts | 153 ++++++++++++++++- .../protonpass/types/protonpass-json-type.ts | 52 +++++- 4 files changed, 448 insertions(+), 3 deletions(-) create mode 100644 libs/importer/spec/test-data/protonpass-json/protonpass-identity.json.ts diff --git a/libs/importer/spec/protonpass-json-importer.spec.ts b/libs/importer/spec/protonpass-json-importer.spec.ts index d5f4653c6434..14519e3ac079 100644 --- a/libs/importer/spec/protonpass-json-importer.spec.ts +++ b/libs/importer/spec/protonpass-json-importer.spec.ts @@ -6,6 +6,7 @@ import { FieldType, CipherType } from "@bitwarden/common/vault/enums"; import { ProtonPassJsonImporter } from "../src/importers"; +import { identityItemTestData } from "./test-data/protonpass-json/protonpass-identity.json"; import { testData } from "./test-data/protonpass-json/protonpass.json"; describe("Protonpass Json Importer", () => { @@ -126,4 +127,94 @@ describe("Protonpass Json Importer", () => { expect(ciphers[1].favorite).toBe(false); expect(ciphers[2].favorite).toBe(true); }); + + it("should skip unsupported items", async () => { + const identityItemTestDataJson = JSON.stringify(identityItemTestData); + const result = await importer.parse(identityItemTestDataJson); + expect(result != null).toBe(true); + + const ciphers = result.ciphers; + expect(ciphers.length).toBe(1); + expect(ciphers[0].type).toEqual(CipherType.Identity); + }); + + it("should parse identity data", async () => { + const identityItemTestDataJson = JSON.stringify(identityItemTestData); + const result = await importer.parse(identityItemTestDataJson); + expect(result != null).toBe(true); + + const cipher = result.ciphers[0]; + + 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); + }); }); diff --git a/libs/importer/spec/test-data/protonpass-json/protonpass-identity.json.ts b/libs/importer/spec/test-data/protonpass-json/protonpass-identity.json.ts new file mode 100644 index 000000000000..91e0eb5156a0 --- /dev/null +++ b/libs/importer/spec/test-data/protonpass-json/protonpass-identity.json.ts @@ -0,0 +1,155 @@ +import { ProtonPassJsonFile } from "../../../src/importers/protonpass/types/protonpass-json-type"; + +export const identityItemTestData: ProtonPassJsonFile = { + encrypted: false, + userId: + "97AKUKQLkB-5BTBjoVgm3es34k8p5_UiKdxLnKb_57b4sekoGHKmKYV__6LMonA_00AKVBzWM-CL1Htkqkwl8Q==", + vaults: { + "TpawpLbs1nuUlQUCtgKZgb3zgAvbrGrOaqOylKqVe_RLROEyUvMq8_ZEuGw73PGRUSr89iNtQ2NosuggP54nwA==": { + name: "Personal", + description: "Personal vault", + display: { color: 0, icon: 0 }, + items: [ + { + 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, + }, + ], + }, + }, + version: "1.22.3", +}; diff --git a/libs/importer/src/importers/protonpass/protonpass-json-importer.ts b/libs/importer/src/importers/protonpass/protonpass-json-importer.ts index b8f6bc170c27..8b9ffc6ac2d5 100644 --- a/libs/importer/src/importers/protonpass/protonpass-json-importer.ts +++ b/libs/importer/src/importers/protonpass/protonpass-json-importer.ts @@ -1,6 +1,8 @@ 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"; @@ -9,16 +11,124 @@ import { Importer } from "../importer"; import { ProtonPassCreditCardItemContent, + ProtonPassIdentityItemContent, + ProtonPassIdentityItemExtraSection, + ProtonPassItemExtraField, ProtonPassItemState, ProtonPassJsonFile, ProtonPassLoginItemContent, } from "./types/protonpass-json-type"; export class ProtonPassJsonImporter extends BaseImporter implements Importer { + private mappedItentityItemKeys = [ + "fullName", + "firstName", + "middleName", + "lastName", + "email", + "phoneNumber", + "company", + "socialSecurityNumber", + "passportNumber", + "licenseNumber", + "organization", + "streetAddress", + "floor", + "county", + "city", + "stateOrProvince", + "zipOrPostalCode", + "countryOrRegion", + ]; + + private itentityItemExtraFieldsKeys = [ + "extraPersonalDetails", + "extraAddressDetails", + "extraContactDetails", + "extraWorkDetails", + "extraSections", + ]; + constructor(private i18nService: I18nService) { super(); } + private processIdentityItemUnmappedAndExtraFields( + cipher: CipherView, + identityItem: ProtonPassIdentityItemContent, + ) { + Object.keys(identityItem).forEach((key) => { + if ( + !this.mappedItentityItemKeys.includes(key) && + !this.itentityItemExtraFieldsKeys.includes(key) + ) { + this.processKvp( + cipher, + key, + identityItem[key as keyof ProtonPassIdentityItemContent] as string, + ); + return; + } + + if (this.itentityItemExtraFieldsKeys.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, + ); + }); + }); + } + } + }); + } + + private processIdentityItemNames( + identity: IdentityView, + 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; + } + + identity.firstName = mappedFirstName; + identity.lastName = mappedLastName; + identity.middleName = mappedMiddleName; + } + parse(data: string): Promise { const result = new ImportResult(); const results: ProtonPassJsonFile = JSON.parse(data); @@ -38,7 +148,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 +205,50 @@ 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 address3 = + `${identityContent.floor ?? ""} ${identityContent.county ?? ""}`.trim(); + this.processIdentityItemNames( + cipher.identity, + this.getValueOrDefault(identityContent.fullName), + this.getValueOrDefault(identityContent.firstName), + this.getValueOrDefault(identityContent.middleName), + this.getValueOrDefault(identityContent.lastName), + ); + 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); + 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); } diff --git a/libs/importer/src/importers/protonpass/types/protonpass-json-type.ts b/libs/importer/src/importers/protonpass/types/protonpass-json-type.ts index eb3b4bba5ac5..20fa314a3144 100644 --- a/libs/importer/src/importers/protonpass/types/protonpass-json-type.ts +++ b/libs/importer/src/importers/protonpass/types/protonpass-json-type.ts @@ -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[]; +}; From 62bcf1efa3930e27eab0d0ec7deb08acd1d6bf4a Mon Sep 17 00:00:00 2001 From: Aftab Ali Date: Wed, 11 Sep 2024 21:20:37 +0530 Subject: [PATCH 2/3] fixed typos and tests --- .../spec/protonpass-json-importer.spec.ts | 24 +-- .../protonpass-identity.json.ts | 155 ------------------ .../protonpass-json/protonpass.json.ts | 138 ++++++++++++++++ .../protonpass/protonpass-json-importer.ts | 16 +- 4 files changed, 160 insertions(+), 173 deletions(-) delete mode 100644 libs/importer/spec/test-data/protonpass-json/protonpass-identity.json.ts diff --git a/libs/importer/spec/protonpass-json-importer.spec.ts b/libs/importer/spec/protonpass-json-importer.spec.ts index 14519e3ac079..39a09127c277 100644 --- a/libs/importer/spec/protonpass-json-importer.spec.ts +++ b/libs/importer/spec/protonpass-json-importer.spec.ts @@ -6,7 +6,6 @@ import { FieldType, CipherType } from "@bitwarden/common/vault/enums"; import { ProtonPassJsonImporter } from "../src/importers"; -import { identityItemTestData } from "./test-data/protonpass-json/protonpass-identity.json"; import { testData } from "./test-data/protonpass-json/protonpass.json"; describe("Protonpass Json Importer", () => { @@ -86,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 () => { @@ -103,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 () => { @@ -115,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 () => { @@ -129,22 +128,25 @@ describe("Protonpass Json Importer", () => { }); it("should skip unsupported items", async () => { - const identityItemTestDataJson = JSON.stringify(identityItemTestData); - const result = await importer.parse(identityItemTestDataJson); + const testDataJson = JSON.stringify(testData); + const result = await importer.parse(testDataJson); expect(result != null).toBe(true); const ciphers = result.ciphers; - expect(ciphers.length).toBe(1); - expect(ciphers[0].type).toEqual(CipherType.Identity); + expect(ciphers.length).toBe(5); + expect(ciphers[4].type).toEqual(CipherType.Login); }); it("should parse identity data", async () => { - const identityItemTestDataJson = JSON.stringify(identityItemTestData); - const result = await importer.parse(identityItemTestDataJson); + const testDataJson = JSON.stringify(testData); + const result = await importer.parse(testDataJson); expect(result != null).toBe(true); - const cipher = result.ciphers[0]; + 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"); diff --git a/libs/importer/spec/test-data/protonpass-json/protonpass-identity.json.ts b/libs/importer/spec/test-data/protonpass-json/protonpass-identity.json.ts deleted file mode 100644 index 91e0eb5156a0..000000000000 --- a/libs/importer/spec/test-data/protonpass-json/protonpass-identity.json.ts +++ /dev/null @@ -1,155 +0,0 @@ -import { ProtonPassJsonFile } from "../../../src/importers/protonpass/types/protonpass-json-type"; - -export const identityItemTestData: ProtonPassJsonFile = { - encrypted: false, - userId: - "97AKUKQLkB-5BTBjoVgm3es34k8p5_UiKdxLnKb_57b4sekoGHKmKYV__6LMonA_00AKVBzWM-CL1Htkqkwl8Q==", - vaults: { - "TpawpLbs1nuUlQUCtgKZgb3zgAvbrGrOaqOylKqVe_RLROEyUvMq8_ZEuGw73PGRUSr89iNtQ2NosuggP54nwA==": { - name: "Personal", - description: "Personal vault", - display: { color: 0, icon: 0 }, - items: [ - { - 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, - }, - ], - }, - }, - version: "1.22.3", -}; diff --git a/libs/importer/spec/test-data/protonpass-json/protonpass.json.ts b/libs/importer/spec/test-data/protonpass-json/protonpass.json.ts index a508a03debc7..367c2b37e14a 100644 --- a/libs/importer/spec/test-data/protonpass-json/protonpass.json.ts +++ b/libs/importer/spec/test-data/protonpass-json/protonpass.json.ts @@ -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: { diff --git a/libs/importer/src/importers/protonpass/protonpass-json-importer.ts b/libs/importer/src/importers/protonpass/protonpass-json-importer.ts index 8b9ffc6ac2d5..f74d0452ca23 100644 --- a/libs/importer/src/importers/protonpass/protonpass-json-importer.ts +++ b/libs/importer/src/importers/protonpass/protonpass-json-importer.ts @@ -20,7 +20,7 @@ import { } from "./types/protonpass-json-type"; export class ProtonPassJsonImporter extends BaseImporter implements Importer { - private mappedItentityItemKeys = [ + private mappedIdentityItemKeys = [ "fullName", "firstName", "middleName", @@ -41,7 +41,7 @@ export class ProtonPassJsonImporter extends BaseImporter implements Importer { "countryOrRegion", ]; - private itentityItemExtraFieldsKeys = [ + private identityItemExtraFieldsKeys = [ "extraPersonalDetails", "extraAddressDetails", "extraContactDetails", @@ -59,8 +59,8 @@ export class ProtonPassJsonImporter extends BaseImporter implements Importer { ) { Object.keys(identityItem).forEach((key) => { if ( - !this.mappedItentityItemKeys.includes(key) && - !this.itentityItemExtraFieldsKeys.includes(key) + !this.mappedIdentityItemKeys.includes(key) && + !this.identityItemExtraFieldsKeys.includes(key) ) { this.processKvp( cipher, @@ -70,7 +70,7 @@ export class ProtonPassJsonImporter extends BaseImporter implements Importer { return; } - if (this.itentityItemExtraFieldsKeys.includes(key)) { + if (this.identityItemExtraFieldsKeys.includes(key)) { if (key !== "extraSections") { const extraFields = identityItem[ key as keyof ProtonPassIdentityItemContent @@ -210,8 +210,6 @@ export class ProtonPassJsonImporter extends BaseImporter implements Importer { cipher.type = CipherType.Identity; cipher.identity = new IdentityView(); - const address3 = - `${identityContent.floor ?? ""} ${identityContent.county ?? ""}`.trim(); this.processIdentityItemNames( cipher.identity, this.getValueOrDefault(identityContent.fullName), @@ -225,9 +223,13 @@ export class ProtonPassJsonImporter extends BaseImporter implements Importer { 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); From 1cf003dda92ca7dcccc1b3c65c3a9c88081b3e10 Mon Sep 17 00:00:00 2001 From: Aftab Ali Date: Thu, 12 Sep 2024 19:39:14 +0530 Subject: [PATCH 3/3] added protonpass utils file --- .../protonpass-import-utils.spec.ts | 66 +++++++++++++++++++ .../protonpass/protonpass-import-utils.ts | 21 ++++++ .../protonpass/protonpass-json-importer.ts | 33 ++-------- 3 files changed, 93 insertions(+), 27 deletions(-) create mode 100644 libs/importer/src/importers/protonpass/protonpass-import-utils.spec.ts create mode 100644 libs/importer/src/importers/protonpass/protonpass-import-utils.ts diff --git a/libs/importer/src/importers/protonpass/protonpass-import-utils.spec.ts b/libs/importer/src/importers/protonpass/protonpass-import-utils.spec.ts new file mode 100644 index 000000000000..efa91eb98f4d --- /dev/null +++ b/libs/importer/src/importers/protonpass/protonpass-import-utils.spec.ts @@ -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", + }); + }); +}); diff --git a/libs/importer/src/importers/protonpass/protonpass-import-utils.ts b/libs/importer/src/importers/protonpass/protonpass-import-utils.ts new file mode 100644 index 000000000000..d8e0a096ab71 --- /dev/null +++ b/libs/importer/src/importers/protonpass/protonpass-import-utils.ts @@ -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 }; +} diff --git a/libs/importer/src/importers/protonpass/protonpass-json-importer.ts b/libs/importer/src/importers/protonpass/protonpass-json-importer.ts index f74d0452ca23..94d21f8521c0 100644 --- a/libs/importer/src/importers/protonpass/protonpass-json-importer.ts +++ b/libs/importer/src/importers/protonpass/protonpass-json-importer.ts @@ -9,6 +9,7 @@ import { ImportResult } from "../../models/import-result"; import { BaseImporter } from "../base-importer"; import { Importer } from "../importer"; +import { processNames } from "./protonpass-import-utils"; import { ProtonPassCreditCardItemContent, ProtonPassIdentityItemContent, @@ -104,31 +105,6 @@ export class ProtonPassJsonImporter extends BaseImporter implements Importer { }); } - private processIdentityItemNames( - identity: IdentityView, - 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; - } - - identity.firstName = mappedFirstName; - identity.lastName = mappedLastName; - identity.middleName = mappedMiddleName; - } - parse(data: string): Promise { const result = new ImportResult(); const results: ProtonPassJsonFile = JSON.parse(data); @@ -210,13 +186,16 @@ export class ProtonPassJsonImporter extends BaseImporter implements Importer { cipher.type = CipherType.Identity; cipher.identity = new IdentityView(); - this.processIdentityItemNames( - cipher.identity, + 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);