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:
@@ -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";
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
};
|
||||
@@ -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)" },
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user