1
0
mirror of https://github.com/bitwarden/browser synced 2025-12-16 00:03:56 +00:00

[PM-2899] Implement ProtonPass json importer (#5766)

* Implement ProtonPass json importer

* Add protonpass-importer json type definition

* Fix alphabetical order in importer imports

* Add importer error message for encrypted protonpass imports

* Add i18n to protonpass importer

* Add protonpass (zip) importer

* Fix protonpass importer

* Add unit tests for protonpass importer

* Make protonpass importer not discard totp codes

* Merge protonpass json & zip importers

* Add protonpass creditcard import & fix note import

* Fix protonpass zip import not recognizing zip files on windows/chrome

* Make protonpass importer use vault types

* Make protonpass importer treat vaults as folders

* Make protonpass importer treat folders as collections for organizations

Co-authored-by: Daniel James Smith <djsmith85@users.noreply.github.com>

* Add types to protonpass test data

* Fix protonpass importer's moveFoldersToCollections

* Add tests for folders/collections

* Remove unecessary type cast in protonpass importer

* Remove unecessary type annotations in protonpass importer

* Add assertion for credit card cvv in protonpass importer

* Handle trashed items in protonpass importer

* Fix setting expiry month on credit cards

* Fix wrong folder-assignment

Only the first item of a "vault" was getting assigned to a folder

Extend unit tests to verify behaviour

---------

Co-authored-by: Daniel James Smith <djsmith85@users.noreply.github.com>
Co-authored-by: Daniel James Smith <djsmith@web.de>
This commit is contained in:
Bernd Schoolmann
2023-08-16 16:17:03 +02:00
committed by GitHub
parent a4fcd62c99
commit e016ed001e
12 changed files with 498 additions and 7 deletions

View File

@@ -44,6 +44,7 @@ export { PasswordBossJsonImporter } from "./passwordboss-json-importer";
export { PasswordDragonXmlImporter } from "./passworddragon-xml-importer";
export { PasswordSafeXmlImporter } from "./passwordsafe-xml-importer";
export { PasswordWalletTxtImporter } from "./passwordwallet-txt-importer";
export { ProtonPassJsonImporter } from "./protonpass/protonpass-json-importer";
export { PsonoJsonImporter } from "./psono/psono-json-importer";
export { RememBearCsvImporter } from "./remembear-csv-importer";
export { RoboFormCsvImporter } from "./roboform-csv-importer";

View File

@@ -0,0 +1,105 @@
import { FieldType, SecureNoteType } from "@bitwarden/common/enums";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { CipherType } from "@bitwarden/common/vault/enums/cipher-type";
import { CardView } from "@bitwarden/common/vault/models/view/card.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 {
ProtonPassCreditCardItemContent,
ProtonPassItemState,
ProtonPassJsonFile,
ProtonPassLoginItemContent,
} from "./types/protonpass-json-type";
export class ProtonPassJsonImporter extends BaseImporter implements Importer {
constructor(private i18nService: I18nService) {
super();
}
parse(data: string): Promise<ImportResult> {
const result = new ImportResult();
const results: ProtonPassJsonFile = JSON.parse(data);
if (results == null || results.vaults == null) {
result.success = false;
return Promise.resolve(result);
}
if (results.encrypted) {
result.success = false;
result.errorMessage = this.i18nService.t("unsupportedEncryptedImport");
return Promise.resolve(result);
}
for (const [, vault] of Object.entries(results.vaults)) {
for (const item of vault.items) {
if (item.state == ProtonPassItemState.TRASHED) {
continue;
}
this.processFolder(result, vault.name);
const cipher = this.initLoginCipher();
cipher.name = item.data.metadata.name;
cipher.notes = item.data.metadata.note;
switch (item.data.type) {
case "login": {
const loginContent = item.data.content as ProtonPassLoginItemContent;
cipher.login.uris = this.makeUriArray(loginContent.urls);
cipher.login.username = loginContent.username;
cipher.login.password = loginContent.password;
if (loginContent.totpUri != "") {
cipher.login.totp = new URL(loginContent.totpUri).searchParams.get("secret");
}
for (const extraField of item.data.extraFields) {
this.processKvp(
cipher,
extraField.fieldName,
extraField.type == "totp" ? extraField.data.totpUri : extraField.data.content,
extraField.type == "text" ? FieldType.Text : FieldType.Hidden
);
}
break;
}
case "note":
cipher.type = CipherType.SecureNote;
cipher.secureNote = new SecureNoteView();
cipher.secureNote.type = SecureNoteType.Generic;
break;
case "creditCard": {
const creditCardContent = item.data.content as ProtonPassCreditCardItemContent;
cipher.type = CipherType.Card;
cipher.card = new CardView();
cipher.card.cardholderName = creditCardContent.cardholderName;
cipher.card.number = creditCardContent.number;
cipher.card.brand = CardView.getCardBrandByPatterns(creditCardContent.number);
cipher.card.code = creditCardContent.verificationNumber;
if (!this.isNullOrWhitespace(creditCardContent.expirationDate)) {
cipher.card.expMonth = creditCardContent.expirationDate.substring(0, 2);
cipher.card.expMonth = cipher.card.expMonth.replace(/^0+/, "");
cipher.card.expYear = creditCardContent.expirationDate.substring(2, 6);
}
if (!this.isNullOrWhitespace(creditCardContent.pin)) {
this.processKvp(cipher, "PIN", creditCardContent.pin, FieldType.Hidden);
}
break;
}
}
this.cleanupCipher(cipher);
result.ciphers.push(cipher);
}
}
if (this.organization) {
this.moveFoldersToCollections(result);
}
result.success = true;
return Promise.resolve(result);
}
}

View File

@@ -0,0 +1,72 @@
export type ProtonPassJsonFile = {
version: string;
userId: string;
encrypted: boolean;
vaults: Record<string, ProtonPassVault>;
};
export type ProtonPassVault = {
name: string;
description: string;
display: {
color: number;
icon: number;
};
items: ProtonPassItem[];
};
export type ProtonPassItem = {
itemId: string;
shareId: string;
data: ProtonPassItemData;
state: ProtonPassItemState;
aliasEmail: string | null;
contentFormatVersion: number;
createTime: number;
modifyTime: number;
};
export enum ProtonPassItemState {
ACTIVE = 1,
TRASHED = 2,
}
export type ProtonPassItemData = {
metadata: ProtonPassItemMetadata;
extraFields: ProtonPassItemExtraField[];
type: "login" | "alias" | "creditCard" | "note";
content: ProtonPassLoginItemContent | ProtonPassCreditCardItemContent;
};
export type ProtonPassItemMetadata = {
name: string;
note: string;
itemUuid: string;
};
export type ProtonPassItemExtraField = {
fieldName: string;
type: string;
data: ProtonPassItemExtraFieldData;
};
export type ProtonPassItemExtraFieldData = {
content?: string;
totpUri?: string;
};
export type ProtonPassLoginItemContent = {
username?: string;
password?: string;
urls?: string[];
totpUri?: string;
};
export type ProtonPassCreditCardItemContent = {
cardholderName?: string;
cardType?: number;
number?: string;
verificationNumber?: string;
expirationDate?: string;
pin?: string;
};

View File

@@ -27,6 +27,7 @@ export const regularImportOptions = [
// { id: "keeperjson", name: "Keeper (json)" },
{ id: "enpasscsv", name: "Enpass (csv)" },
{ id: "enpassjson", name: "Enpass (json)" },
{ id: "protonpass", name: "ProtonPass (zip/json)" },
{ id: "safeincloudxml", name: "SafeInCloud (xml)" },
{ id: "pwsafexml", name: "Password Safe (xml)" },
{ id: "stickypasswordxml", name: "Sticky Password (xml)" },

View File

@@ -62,6 +62,7 @@ import {
PasswordDragonXmlImporter,
PasswordSafeXmlImporter,
PasswordWalletTxtImporter,
ProtonPassJsonImporter,
PsonoJsonImporter,
RememBearCsvImporter,
RoboFormCsvImporter,
@@ -319,6 +320,8 @@ export class ImportService implements ImportServiceAbstraction {
return new PsonoJsonImporter();
case "passkyjson":
return new PasskyJsonImporter();
case "protonpass":
return new ProtonPassJsonImporter(this.i18nService);
default:
return null;
}