mirror of
https://github.com/bitwarden/browser.git
synced 2025-01-02 18:17:46 +01: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:
parent
a4fcd62c99
commit
e016ed001e
@ -46,5 +46,8 @@
|
|||||||
},
|
},
|
||||||
"ssoKeyConnectorError": {
|
"ssoKeyConnectorError": {
|
||||||
"message": "Key Connector error: make sure Key Connector is available and working correctly."
|
"message": "Key Connector error: make sure Key Connector is available and working correctly."
|
||||||
|
},
|
||||||
|
"unsupportedEncryptedImport": {
|
||||||
|
"message": "Importing encrypted files is currently not supported."
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -67,7 +67,9 @@ export class ImportCommand {
|
|||||||
try {
|
try {
|
||||||
let contents;
|
let contents;
|
||||||
if (format === "1password1pux") {
|
if (format === "1password1pux") {
|
||||||
contents = await CliUtils.extract1PuxContent(filepath);
|
contents = await CliUtils.extractZipContent(filepath, "export.data");
|
||||||
|
} else if (format === "protonpass" && filepath.endsWith(".zip")) {
|
||||||
|
contents = await CliUtils.extractZipContent(filepath, "Proton Pass/data.json");
|
||||||
} else {
|
} else {
|
||||||
contents = await CliUtils.readFile(filepath);
|
contents = await CliUtils.readFile(filepath);
|
||||||
}
|
}
|
||||||
|
@ -46,7 +46,7 @@ export class CliUtils {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
static extract1PuxContent(input: string): Promise<string> {
|
static extractZipContent(input: string, filepath: string): Promise<string> {
|
||||||
return new Promise<string>((resolve, reject) => {
|
return new Promise<string>((resolve, reject) => {
|
||||||
let p: string = null;
|
let p: string = null;
|
||||||
if (input != null && input !== "") {
|
if (input != null && input !== "") {
|
||||||
@ -65,7 +65,7 @@ export class CliUtils {
|
|||||||
}
|
}
|
||||||
JSZip.loadAsync(data).then(
|
JSZip.loadAsync(data).then(
|
||||||
(zip) => {
|
(zip) => {
|
||||||
resolve(zip.file("export.data").async("string"));
|
resolve(zip.file(filepath).async("string"));
|
||||||
},
|
},
|
||||||
(reason) => {
|
(reason) => {
|
||||||
reject(reason);
|
reject(reason);
|
||||||
@ -74,6 +74,7 @@ export class CliUtils {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Save the given data to a file and determine the target file if necessary.
|
* Save the given data to a file and determine the target file if necessary.
|
||||||
* If output is non-empty, it is used as target filename. Otherwise the target filename is
|
* If output is non-empty, it is used as target filename. Otherwise the target filename is
|
||||||
|
@ -341,6 +341,10 @@
|
|||||||
Log in to "https://vault.passky.org" → "Import & Export" → "Export" in the Passky
|
Log in to "https://vault.passky.org" → "Import & Export" → "Export" in the Passky
|
||||||
section. ("Backup" is unsupported as it is encrypted).
|
section. ("Backup" is unsupported as it is encrypted).
|
||||||
</ng-container>
|
</ng-container>
|
||||||
|
<ng-container *ngIf="format === 'protonpass'">
|
||||||
|
In the ProtonPass browser extension, go to Settings > Export. Export without PGP encryption
|
||||||
|
and save the zip file.
|
||||||
|
</ng-container>
|
||||||
</bit-callout>
|
</bit-callout>
|
||||||
<bit-form-field>
|
<bit-form-field>
|
||||||
<bit-label>{{ "selectImportFile" | i18n }}</bit-label>
|
<bit-label>{{ "selectImportFile" | i18n }}</bit-label>
|
||||||
|
@ -326,7 +326,15 @@ export class ImportComponent implements OnInit, OnDestroy {
|
|||||||
|
|
||||||
private getFileContents(file: File): Promise<string> {
|
private getFileContents(file: File): Promise<string> {
|
||||||
if (this.format === "1password1pux") {
|
if (this.format === "1password1pux") {
|
||||||
return this.extract1PuxContent(file);
|
return this.extractZipContent(file, "export.data");
|
||||||
|
}
|
||||||
|
if (
|
||||||
|
this.format === "protonpass" &&
|
||||||
|
(file.type === "application/zip" ||
|
||||||
|
file.type == "application/x-zip-compressed" ||
|
||||||
|
file.name.endsWith(".zip"))
|
||||||
|
) {
|
||||||
|
return this.extractZipContent(file, "Proton Pass/data.json");
|
||||||
}
|
}
|
||||||
|
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
@ -353,11 +361,11 @@ export class ImportComponent implements OnInit, OnDestroy {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
private extract1PuxContent(file: File): Promise<string> {
|
private extractZipContent(zipFile: File, contentFilePath: string): Promise<string> {
|
||||||
return new JSZip()
|
return new JSZip()
|
||||||
.loadAsync(file)
|
.loadAsync(zipFile)
|
||||||
.then((zip) => {
|
.then((zip) => {
|
||||||
return zip.file("export.data").async("string");
|
return zip.file(contentFilePath).async("string");
|
||||||
})
|
})
|
||||||
.then(
|
.then(
|
||||||
function success(content) {
|
function success(content) {
|
||||||
|
117
libs/importer/spec/protonpass-json-importer.spec.ts
Normal file
117
libs/importer/spec/protonpass-json-importer.spec.ts
Normal file
@ -0,0 +1,117 @@
|
|||||||
|
import { MockProxy } from "jest-mock-extended";
|
||||||
|
|
||||||
|
import { FieldType } from "@bitwarden/common/enums";
|
||||||
|
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||||
|
import { Utils } from "@bitwarden/common/platform/misc/utils";
|
||||||
|
import { CipherType } from "@bitwarden/common/vault/enums/cipher-type";
|
||||||
|
|
||||||
|
import { ProtonPassJsonImporter } from "../src/importers";
|
||||||
|
|
||||||
|
import { testData } from "./test-data/protonpass-json/protonpass.json";
|
||||||
|
|
||||||
|
describe("Protonpass Json Importer", () => {
|
||||||
|
let importer: ProtonPassJsonImporter;
|
||||||
|
let i18nService: MockProxy<I18nService>;
|
||||||
|
beforeEach(() => {
|
||||||
|
importer = new ProtonPassJsonImporter(i18nService);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should parse login data", async () => {
|
||||||
|
const testDataJson = JSON.stringify(testData);
|
||||||
|
|
||||||
|
const result = await importer.parse(testDataJson);
|
||||||
|
expect(result != null).toBe(true);
|
||||||
|
|
||||||
|
const cipher = result.ciphers.shift();
|
||||||
|
expect(cipher.name).toEqual("Test Login - Personal Vault");
|
||||||
|
expect(cipher.type).toEqual(CipherType.Login);
|
||||||
|
expect(cipher.login.username).toEqual("Username");
|
||||||
|
expect(cipher.login.password).toEqual("Password");
|
||||||
|
expect(cipher.login.uris.length).toEqual(2);
|
||||||
|
const uriView = cipher.login.uris.shift();
|
||||||
|
expect(uriView.uri).toEqual("https://example.com/");
|
||||||
|
expect(cipher.notes).toEqual("My login secure note.");
|
||||||
|
|
||||||
|
expect(cipher.fields.at(2).name).toEqual("second 2fa secret");
|
||||||
|
expect(cipher.fields.at(2).value).toEqual("TOTPCODE");
|
||||||
|
expect(cipher.fields.at(2).type).toEqual(FieldType.Hidden);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should parse note data", async () => {
|
||||||
|
const testDataJson = JSON.stringify(testData);
|
||||||
|
|
||||||
|
const result = await importer.parse(testDataJson);
|
||||||
|
expect(result != null).toBe(true);
|
||||||
|
|
||||||
|
result.ciphers.shift();
|
||||||
|
const noteCipher = result.ciphers.shift();
|
||||||
|
expect(noteCipher.type).toEqual(CipherType.SecureNote);
|
||||||
|
expect(noteCipher.name).toEqual("My Secure Note");
|
||||||
|
expect(noteCipher.notes).toEqual("Secure note contents.");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should parse credit card data", async () => {
|
||||||
|
const testDataJson = JSON.stringify(testData);
|
||||||
|
|
||||||
|
const result = await importer.parse(testDataJson);
|
||||||
|
expect(result != null).toBe(true);
|
||||||
|
|
||||||
|
result.ciphers.shift();
|
||||||
|
result.ciphers.shift();
|
||||||
|
|
||||||
|
const creditCardCipher = result.ciphers.shift();
|
||||||
|
expect(creditCardCipher.type).toBe(CipherType.Card);
|
||||||
|
expect(creditCardCipher.card.number).toBe("1234222233334444");
|
||||||
|
expect(creditCardCipher.card.cardholderName).toBe("Test name");
|
||||||
|
expect(creditCardCipher.card.expMonth).toBe("1");
|
||||||
|
expect(creditCardCipher.card.expYear).toBe("2025");
|
||||||
|
expect(creditCardCipher.card.code).toBe("333");
|
||||||
|
expect(creditCardCipher.fields.at(0).name).toEqual("PIN");
|
||||||
|
expect(creditCardCipher.fields.at(0).value).toEqual("1234");
|
||||||
|
expect(creditCardCipher.fields.at(0).type).toEqual(FieldType.Hidden);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should create folders if not part of an organization", async () => {
|
||||||
|
const testDataJson = JSON.stringify(testData);
|
||||||
|
const result = await importer.parse(testDataJson);
|
||||||
|
|
||||||
|
const folders = result.folders;
|
||||||
|
expect(folders.length).toBe(2);
|
||||||
|
expect(folders[0].name).toBe("Personal");
|
||||||
|
expect(folders[1].name).toBe("Test");
|
||||||
|
|
||||||
|
// "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]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should create collections if part of an organization", async () => {
|
||||||
|
const testDataJson = JSON.stringify(testData);
|
||||||
|
importer.organizationId = Utils.newGuid();
|
||||||
|
const result = await importer.parse(testDataJson);
|
||||||
|
expect(result != null).toBe(true);
|
||||||
|
|
||||||
|
const collections = result.collections;
|
||||||
|
expect(collections.length).toBe(2);
|
||||||
|
expect(collections[0].name).toBe("Personal");
|
||||||
|
expect(collections[1].name).toBe("Test");
|
||||||
|
|
||||||
|
// "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]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should not add deleted items", async () => {
|
||||||
|
const testDataJson = JSON.stringify(testData);
|
||||||
|
const result = await importer.parse(testDataJson);
|
||||||
|
|
||||||
|
const ciphers = result.ciphers;
|
||||||
|
for (const cipher of ciphers) {
|
||||||
|
expect(cipher.name).not.toBe("My Deleted Note");
|
||||||
|
}
|
||||||
|
|
||||||
|
expect(ciphers.length).toBe(4);
|
||||||
|
});
|
||||||
|
});
|
174
libs/importer/spec/test-data/protonpass-json/protonpass.json.ts
Normal file
174
libs/importer/spec/test-data/protonpass-json/protonpass.json.ts
Normal file
@ -0,0 +1,174 @@
|
|||||||
|
import { ProtonPassJsonFile } from "../../../src/importers/protonpass/types/protonpass-json-type";
|
||||||
|
|
||||||
|
export const testData: ProtonPassJsonFile = {
|
||||||
|
version: "1.3.1",
|
||||||
|
userId: "REDACTED_USER_ID",
|
||||||
|
encrypted: false,
|
||||||
|
vaults: {
|
||||||
|
REDACTED_VAULT_ID_A: {
|
||||||
|
name: "Personal",
|
||||||
|
description: "Personal vault",
|
||||||
|
display: {
|
||||||
|
color: 0,
|
||||||
|
icon: 0,
|
||||||
|
},
|
||||||
|
items: [
|
||||||
|
{
|
||||||
|
itemId:
|
||||||
|
"yZENmDjtmZGODNy3Q_CZiPAF_IgINq8w-R-qazrOh-Nt9YJeVF3gu07ovzDS4jhYHoMdOebTw5JkYPGgIL1mwQ==",
|
||||||
|
shareId:
|
||||||
|
"SN5uWo4WZF2uT5wIDqtbdpkjuxCbNTOIdf-JQ_DYZcKYKURHiZB5csS1a1p9lklvju9ni42l08IKzwQG0B2ySg==",
|
||||||
|
data: {
|
||||||
|
metadata: {
|
||||||
|
name: "Test Login - Personal Vault",
|
||||||
|
note: "My login secure note.",
|
||||||
|
itemUuid: "e8ee1a0c",
|
||||||
|
},
|
||||||
|
extraFields: [
|
||||||
|
{
|
||||||
|
fieldName: "non-hidden field",
|
||||||
|
type: "text",
|
||||||
|
data: {
|
||||||
|
content: "non-hidden field content",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
fieldName: "hidden field",
|
||||||
|
type: "hidden",
|
||||||
|
data: {
|
||||||
|
content: "hidden field content",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
fieldName: "second 2fa secret",
|
||||||
|
type: "totp",
|
||||||
|
data: {
|
||||||
|
totpUri: "TOTPCODE",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
type: "login",
|
||||||
|
content: {
|
||||||
|
username: "Username",
|
||||||
|
password: "Password",
|
||||||
|
urls: ["https://example.com/", "https://example2.com/"],
|
||||||
|
totpUri:
|
||||||
|
"otpauth://totp/Test%20Login%20-%20Personal%20Vault:Username?issuer=Test%20Login%20-%20Personal%20Vault&secret=TOTPCODE&algorithm=SHA1&digits=6&period=30",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
state: 1,
|
||||||
|
aliasEmail: null,
|
||||||
|
contentFormatVersion: 1,
|
||||||
|
createTime: 1689182868,
|
||||||
|
modifyTime: 1689182868,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
itemId:
|
||||||
|
"xqq_Bh8RxNMBerkiMvRdH427yswZznjYwps-f6C5D8tmKiPgMxCSPNz1BOd4nRJ309gciDiPhXcCVWOyfJ66ZA==",
|
||||||
|
shareId:
|
||||||
|
"SN5uWo4WZF2uT5wIDqtbdpkjuxCbNTOIdf-JQ_DYZcKYKURHiZB5csS1a1p9lklvju9ni42l08IKzwQG0B2ySg==",
|
||||||
|
data: {
|
||||||
|
metadata: {
|
||||||
|
name: "My Secure Note",
|
||||||
|
note: "Secure note contents.",
|
||||||
|
itemUuid: "ad618070",
|
||||||
|
},
|
||||||
|
extraFields: [],
|
||||||
|
type: "note",
|
||||||
|
content: {},
|
||||||
|
},
|
||||||
|
state: 1,
|
||||||
|
aliasEmail: null,
|
||||||
|
contentFormatVersion: 1,
|
||||||
|
createTime: 1689182908,
|
||||||
|
modifyTime: 1689182908,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
itemId:
|
||||||
|
"ZmGzd-HNQYTr6wmfWlSfiStXQLqGic_PYB2Q2T_hmuRM2JIA4pKAPJcmFafxJrDpXxLZ2EPjgD6Noc9a0U6AVQ==",
|
||||||
|
shareId:
|
||||||
|
"SN5uWo4WZF2uT5wIDqtbdpkjuxCbNTOIdf-JQ_DYZcKYKURHiZB5csS1a1p9lklvju9ni42l08IKzwQG0B2ySg==",
|
||||||
|
data: {
|
||||||
|
metadata: {
|
||||||
|
name: "Test Card",
|
||||||
|
note: "Credit Card Note",
|
||||||
|
itemUuid: "d8f45370",
|
||||||
|
},
|
||||||
|
extraFields: [],
|
||||||
|
type: "creditCard",
|
||||||
|
content: {
|
||||||
|
cardholderName: "Test name",
|
||||||
|
cardType: 0,
|
||||||
|
number: "1234222233334444",
|
||||||
|
verificationNumber: "333",
|
||||||
|
expirationDate: "012025",
|
||||||
|
pin: "1234",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
state: 1,
|
||||||
|
aliasEmail: null,
|
||||||
|
contentFormatVersion: 1,
|
||||||
|
createTime: 1691001643,
|
||||||
|
modifyTime: 1691001643,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
itemId:
|
||||||
|
"xqq_Bh8RxNMBerkiMvRdH427yswZznjYwps-f6C5D8tmKiPgMxCSPNz1BOd4nRJ309gciDiPhXcCVWOyfJ66ZA==",
|
||||||
|
shareId:
|
||||||
|
"SN5uWo4WZF2uT5wIDqtbdpkjuxCbNTOIdf-JQ_DYZcKYKURHiZB5csS1a1p9lklvju9ni42l08IKzwQG0B2ySg==",
|
||||||
|
data: {
|
||||||
|
metadata: {
|
||||||
|
name: "My Deleted Note",
|
||||||
|
note: "Secure note contents.",
|
||||||
|
itemUuid: "ad618070",
|
||||||
|
},
|
||||||
|
extraFields: [],
|
||||||
|
type: "note",
|
||||||
|
content: {},
|
||||||
|
},
|
||||||
|
state: 2,
|
||||||
|
aliasEmail: null,
|
||||||
|
contentFormatVersion: 1,
|
||||||
|
createTime: 1689182908,
|
||||||
|
modifyTime: 1689182908,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
REDACTED_VAULT_ID_B: {
|
||||||
|
name: "Test",
|
||||||
|
description: "",
|
||||||
|
display: {
|
||||||
|
color: 4,
|
||||||
|
icon: 2,
|
||||||
|
},
|
||||||
|
items: [
|
||||||
|
{
|
||||||
|
itemId:
|
||||||
|
"U_J8-eUR15sC-PjUhjVcixDcayhjGuoerUZCr560RlAi0ZjBNkSaSKAytVzZn4E0hiFX1_y4qZbUetl6jO3aJw==",
|
||||||
|
shareId:
|
||||||
|
"OJz-4MnPqAuYnyemhctcGDlSLJrzsTnf2FnFSwxh1QP_oth9xyGDc2ZAqCv5FnqkVgTNHT5aPj62zcekNemfNw==",
|
||||||
|
data: {
|
||||||
|
metadata: {
|
||||||
|
name: "Other vault login",
|
||||||
|
note: "",
|
||||||
|
itemUuid: "f3429d44",
|
||||||
|
},
|
||||||
|
extraFields: [],
|
||||||
|
type: "login",
|
||||||
|
content: {
|
||||||
|
username: "other vault username",
|
||||||
|
password: "other vault password",
|
||||||
|
urls: [],
|
||||||
|
totpUri: "",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
state: 1,
|
||||||
|
aliasEmail: null,
|
||||||
|
contentFormatVersion: 1,
|
||||||
|
createTime: 1689182949,
|
||||||
|
modifyTime: 1689182949,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
@ -44,6 +44,7 @@ export { PasswordBossJsonImporter } from "./passwordboss-json-importer";
|
|||||||
export { PasswordDragonXmlImporter } from "./passworddragon-xml-importer";
|
export { PasswordDragonXmlImporter } from "./passworddragon-xml-importer";
|
||||||
export { PasswordSafeXmlImporter } from "./passwordsafe-xml-importer";
|
export { PasswordSafeXmlImporter } from "./passwordsafe-xml-importer";
|
||||||
export { PasswordWalletTxtImporter } from "./passwordwallet-txt-importer";
|
export { PasswordWalletTxtImporter } from "./passwordwallet-txt-importer";
|
||||||
|
export { ProtonPassJsonImporter } from "./protonpass/protonpass-json-importer";
|
||||||
export { PsonoJsonImporter } from "./psono/psono-json-importer";
|
export { PsonoJsonImporter } from "./psono/psono-json-importer";
|
||||||
export { RememBearCsvImporter } from "./remembear-csv-importer";
|
export { RememBearCsvImporter } from "./remembear-csv-importer";
|
||||||
export { RoboFormCsvImporter } from "./roboform-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: "keeperjson", name: "Keeper (json)" },
|
||||||
{ id: "enpasscsv", name: "Enpass (csv)" },
|
{ id: "enpasscsv", name: "Enpass (csv)" },
|
||||||
{ id: "enpassjson", name: "Enpass (json)" },
|
{ id: "enpassjson", name: "Enpass (json)" },
|
||||||
|
{ id: "protonpass", name: "ProtonPass (zip/json)" },
|
||||||
{ id: "safeincloudxml", name: "SafeInCloud (xml)" },
|
{ id: "safeincloudxml", name: "SafeInCloud (xml)" },
|
||||||
{ id: "pwsafexml", name: "Password Safe (xml)" },
|
{ id: "pwsafexml", name: "Password Safe (xml)" },
|
||||||
{ id: "stickypasswordxml", name: "Sticky Password (xml)" },
|
{ id: "stickypasswordxml", name: "Sticky Password (xml)" },
|
||||||
|
@ -62,6 +62,7 @@ import {
|
|||||||
PasswordDragonXmlImporter,
|
PasswordDragonXmlImporter,
|
||||||
PasswordSafeXmlImporter,
|
PasswordSafeXmlImporter,
|
||||||
PasswordWalletTxtImporter,
|
PasswordWalletTxtImporter,
|
||||||
|
ProtonPassJsonImporter,
|
||||||
PsonoJsonImporter,
|
PsonoJsonImporter,
|
||||||
RememBearCsvImporter,
|
RememBearCsvImporter,
|
||||||
RoboFormCsvImporter,
|
RoboFormCsvImporter,
|
||||||
@ -319,6 +320,8 @@ export class ImportService implements ImportServiceAbstraction {
|
|||||||
return new PsonoJsonImporter();
|
return new PsonoJsonImporter();
|
||||||
case "passkyjson":
|
case "passkyjson":
|
||||||
return new PasskyJsonImporter();
|
return new PasskyJsonImporter();
|
||||||
|
case "protonpass":
|
||||||
|
return new ProtonPassJsonImporter(this.i18nService);
|
||||||
default:
|
default:
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user