1
0
mirror of https://github.com/bitwarden/browser synced 2025-12-15 07:43:35 +00:00

[PS-2096] BEEEEP: Add Psono importer (#4286)

* Add psono json importer

Create types for psono export format
Add test files
Write tests for psono-json-importer
Write importer for psono export
Register 'psonojson' with `importOptions`
Import/register psono-json-importer with import.service
Add instructions on how to export from Psono

* Retain all imported data

Ensure all data is retained by adding unmapped properties into custom fields
Each item type has a set of mapped properties, anything not matching will be created as a custom field
Write extensive tests to ensure data is present

* Skipping GPG

We currently cannot import GPG Keys into notes or custom fields

* Add organizational test

Verify that folders get converted to collections when imported by an org

* Remove combined test-file (whole export)

* Remove redundant null type
This commit is contained in:
Daniel James Smith
2023-01-30 13:56:49 +01:00
committed by GitHub
parent 651968ca9c
commit b1a1068906
15 changed files with 840 additions and 0 deletions

View File

@@ -288,6 +288,10 @@
From the Yoti browser extension, click on "Settings", then "Export Saved Logins" and save the From the Yoti browser extension, click on "Settings", then "Export Saved Logins" and save the
CSV file. CSV file.
</ng-container> </ng-container>
<ng-container *ngIf="format === 'psonojson'">
Log in to the Psono web vault, click on the "Signed in as"-dropdown, select "Others". Go to
the "Export"-tab and select the json type export and then click on Export.
</ng-container>
<ng-container *ngIf="format === 'passkyjson'"> <ng-container *ngIf="format === 'passkyjson'">
Log in to "https://vault.passky.org" &rarr; "Import & Export" &rarr; "Export" in the Passky Log in to "https://vault.passky.org" &rarr; "Import & Export" &rarr; "Export" in the Passky
section. ("Backup" is unsupported as it is encrypted). section. ("Backup" is unsupported as it is encrypted).

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,20 @@
import { PsonoJsonExport } from "@bitwarden/common/importers/psono/psono-json-types";
export const ApplicationPasswordsData: PsonoJsonExport = {
folders: [],
items: [
{
type: "application_password",
name: "My App Password",
application_password_title: "My App Password",
application_password_username: "someUser",
application_password_password: "somePassword",
application_password_notes: "some notes for the APP",
create_date: "2022-12-13T19:42:05.784077Z",
write_date: "2022-12-13T19:42:05.784103Z",
callback_url: "",
callback_user: "",
callback_pass: "",
},
],
};

View File

@@ -0,0 +1,21 @@
import { PsonoJsonExport } from "@bitwarden/common/importers/psono/psono-json-types";
export const BookmarkData: PsonoJsonExport = {
folders: [],
items: [
{
type: "bookmark",
name: "MyBookmark",
urlfilter: "bitwarden.com",
bookmark_title: "MyBookmark",
bookmark_url: "https://bitwarden.com",
bookmark_notes: "my notes for bitwarden.com",
bookmark_url_filter: "bitwarden.com",
create_date: "2022-12-13T19:39:26.631530Z",
write_date: "2022-12-13T19:39:26.631553Z",
callback_url: "",
callback_user: "",
callback_pass: "",
},
],
};

View File

@@ -0,0 +1,10 @@
import { PsonoJsonExport } from "@bitwarden/common/importers/psono/psono-json-types";
export const EmptyTestFolderData: PsonoJsonExport = {
folders: [
{
name: "EmptyFolder",
items: [],
},
],
};

View File

@@ -0,0 +1,22 @@
import { PsonoJsonExport } from "@bitwarden/common/importers/psono/psono-json-types";
export const EnvVariablesData: PsonoJsonExport = {
folders: [],
items: [
{
type: "environment_variables",
name: "My Environment Variables",
environment_variables_title: "My Environment Variables",
environment_variables_variables: [
{ key: "Key1", value: "Value1" },
{ key: "Key2", value: "Value2" },
],
environment_variables_notes: "Notes for environment variables",
create_date: "2022-12-13T19:41:02.028884Z",
write_date: "2022-12-13T19:41:02.028909Z",
callback_url: "",
callback_user: "",
callback_pass: "",
},
],
};

View File

@@ -0,0 +1,53 @@
import { PsonoJsonExport } from "@bitwarden/common/importers/psono/psono-json-types";
export const FoldersTestData: PsonoJsonExport = {
folders: [
{
name: "TestFolder",
items: [
{
type: "website_password",
name: "TestEntry",
autosubmit: true,
urlfilter: "filter",
website_password_title: "TestEntry",
website_password_url: "bitwarden.com",
website_password_username: "testUser",
website_password_password: "testPassword",
website_password_notes: "some notes",
website_password_auto_submit: true,
website_password_url_filter: "filter",
create_date: "2022-12-13T19:24:09.810266Z",
write_date: "2022-12-13T19:24:09.810292Z",
callback_url: "callback",
callback_user: "callbackUser",
callback_pass: "callbackPassword",
},
],
},
{
name: "TestFolder2",
items: [
{
type: "website_password",
name: "TestEntry2",
autosubmit: true,
urlfilter: "filter",
website_password_title: "TestEntry2",
website_password_url: "bitwarden.com",
website_password_username: "testUser",
website_password_password: "testPassword",
website_password_notes: "some notes",
website_password_auto_submit: true,
website_password_url_filter: "filter",
create_date: "2022-12-13T19:24:09.810266Z",
write_date: "2022-12-13T19:24:09.810292Z",
callback_url: "callback",
callback_user: "callbackUser",
callback_pass: "callbackPassword",
},
],
},
],
items: [],
};

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,18 @@
import { PsonoJsonExport } from "@bitwarden/common/importers/psono/psono-json-types";
export const NotesData: PsonoJsonExport = {
folders: [],
items: [
{
type: "note",
name: "My Note",
note_title: "My Note",
note_notes: "Notes for my Note",
create_date: "2022-12-13T19:41:18.770714Z",
write_date: "2022-12-13T19:41:18.770738Z",
callback_url: "",
callback_user: "",
callback_pass: "",
},
],
};

View File

@@ -0,0 +1,22 @@
import { PsonoJsonExport } from "@bitwarden/common/importers/psono/psono-json-types";
export const TOTPData: PsonoJsonExport = {
folders: [],
items: [
{
type: "totp",
name: "My TOTP",
totp_title: "My TOTP",
totp_period: 30,
totp_algorithm: "SHA1",
totp_digits: 6,
totp_code: "someSecretOfMine",
totp_notes: "Notes for TOTP",
create_date: "2022-12-13T19:41:42.972586Z",
write_date: "2022-12-13T19:41:42.972609Z",
callback_url: "",
callback_user: "",
callback_pass: "",
},
],
};

View File

@@ -0,0 +1,25 @@
import { PsonoJsonExport } from "@bitwarden/common/importers/psono/psono-json-types";
export const WebsiteLoginsData: PsonoJsonExport = {
folders: [],
items: [
{
type: "website_password",
name: "TestEntry",
autosubmit: true,
urlfilter: "filter",
website_password_title: "TestEntry",
website_password_url: "bitwarden.com",
website_password_username: "testUser",
website_password_password: "testPassword",
website_password_notes: "some notes",
website_password_auto_submit: true,
website_password_url_filter: "filter",
create_date: "2022-12-13T19:24:09.810266Z",
write_date: "2022-12-13T19:24:09.810292Z",
callback_url: "callback",
callback_user: "callbackUser",
callback_pass: "callbackPassword",
},
],
};

View File

@@ -67,6 +67,7 @@ export const regularImportOptions = [
{ id: "encryptrcsv", name: "Encryptr (csv)" }, { id: "encryptrcsv", name: "Encryptr (csv)" },
{ id: "yoticsv", name: "Yoti (csv)" }, { id: "yoticsv", name: "Yoti (csv)" },
{ id: "nordpasscsv", name: "Nordpass (csv)" }, { id: "nordpasscsv", name: "Nordpass (csv)" },
{ id: "psonojson", name: "Psono (json)" },
{ id: "passkyjson", name: "Passky (json)" }, { id: "passkyjson", name: "Passky (json)" },
] as const; ] as const;

View File

@@ -0,0 +1,281 @@
import { CipherType } from "../../enums/cipherType";
import { FieldType } from "../../enums/fieldType";
import { SecureNoteType } from "../../enums/secureNoteType";
import { ImportResult } from "../../models/domain/import-result";
import { CipherView } from "../../models/view/cipher.view";
import { SecureNoteView } from "../../models/view/secure-note.view";
import { BaseImporter } from "../base-importer";
import { Importer } from "../importer";
import {
AppPasswordEntry,
BookmarkEntry,
EnvironmentVariablesEntry,
FoldersEntity,
GPGEntry,
NotesEntry,
PsonoItemTypes,
PsonoJsonExport,
TOTPEntry,
WebsitePasswordEntry,
} from "./psono-json-types";
export class PsonoJsonImporter extends BaseImporter implements Importer {
parse(data: string): Promise<ImportResult> {
const result = new ImportResult();
const psonoExport: PsonoJsonExport = JSON.parse(data);
if (psonoExport == null) {
result.success = false;
return Promise.resolve(result);
}
this.parseFolders(result, psonoExport.folders);
this.handleItemParsing(result, psonoExport.items);
if (this.organization) {
this.moveFoldersToCollections(result);
}
result.success = true;
return Promise.resolve(result);
}
private parseFolders(result: ImportResult, folders: FoldersEntity[]) {
if (folders == null || folders.length === 0) {
return;
}
folders.forEach((folder) => {
if (folder.items == null || folder.items.length == 0) {
return;
}
this.processFolder(result, folder.name);
this.handleItemParsing(result, folder.items);
});
}
private handleItemParsing(result: ImportResult, items?: PsonoItemTypes[]) {
if (items == null || items.length === 0) {
return;
}
items.forEach((record) => {
const cipher = this.parsePsonoItem(record);
this.cleanupCipher(cipher);
result.ciphers.push(cipher);
});
}
private parsePsonoItem(item: PsonoItemTypes): CipherView {
const cipher = this.initLoginCipher();
switch (item.type) {
case "website_password":
this.parseWebsiteLogins(item, cipher);
break;
case "application_password":
this.parseApplicationPasswords(item, cipher);
break;
case "environment_variables":
this.parseEnvironmentVariables(item, cipher);
break;
case "totp":
this.parseTOTP(item, cipher);
break;
case "bookmark":
this.parseBookmarks(item, cipher);
break;
// Skipping this until we can save GPG into notes/custom fields
// case "mail_gpg_own_key":
// this.parseGPG(item, cipher);
// break;
case "note":
this.parseNotes(item, cipher);
break;
default:
break;
}
return cipher;
}
readonly WEBSITE_mappedValues = new Set([
"type",
"name",
"website_password_title",
"website_password_notes",
"website_password_username",
"website_password_password",
"website_password_url",
"autosubmit",
"website_password_auto_submit",
"urlfilter",
"website_password_url_filter",
]);
private parseWebsiteLogins(entry: WebsitePasswordEntry, cipher: CipherView) {
if (entry == null || entry.type != "website_password") {
return;
}
cipher.name = entry.website_password_title;
cipher.notes = entry.website_password_notes;
cipher.login.username = entry.website_password_username;
cipher.login.password = entry.website_password_password;
cipher.login.uris = this.makeUriArray(entry.website_password_url);
this.processKvp(
cipher,
"website_password_auto_submit",
entry.website_password_auto_submit.toString(),
FieldType.Boolean
);
this.processKvp(cipher, "website_password_url_filter", entry.website_password_url_filter);
this.importUnmappedFields(cipher, entry, this.WEBSITE_mappedValues);
}
readonly APP_PWD_mappedValues = new Set([
"type",
"name",
"application_password_title",
"application_password_notes",
"application_password_username",
"application_password_password",
]);
private parseApplicationPasswords(entry: AppPasswordEntry, cipher: CipherView) {
if (entry == null || entry.type != "application_password") {
return;
}
cipher.name = entry.application_password_title;
cipher.notes = entry.application_password_notes;
cipher.login.username = entry.application_password_username;
cipher.login.password = entry.application_password_password;
this.importUnmappedFields(cipher, entry, this.APP_PWD_mappedValues);
}
readonly BOOKMARK_mappedValues = new Set([
"type",
"name",
"bookmark_title",
"bookmark_notes",
"bookmark_url",
]);
private parseBookmarks(entry: BookmarkEntry, cipher: CipherView) {
if (entry == null || entry.type != "bookmark") {
return;
}
cipher.name = entry.bookmark_title;
cipher.notes = entry.bookmark_notes;
cipher.login.uris = this.makeUriArray(entry.bookmark_url);
this.importUnmappedFields(cipher, entry, this.BOOKMARK_mappedValues);
}
readonly NOTES_mappedValues = new Set(["type", "name", "note_title", "note_notes"]);
private parseNotes(entry: NotesEntry, cipher: CipherView) {
if (entry == null || entry.type != "note") {
return;
}
cipher.type = CipherType.SecureNote;
cipher.secureNote = new SecureNoteView();
cipher.secureNote.type = SecureNoteType.Generic;
cipher.name = entry.note_title;
cipher.notes = entry.note_notes;
this.importUnmappedFields(cipher, entry, this.NOTES_mappedValues);
}
readonly TOTP_mappedValues = new Set(["type", "name", "totp_title", "totp_notes", "totp_code"]);
private parseTOTP(entry: TOTPEntry, cipher: CipherView) {
if (entry == null || entry.type != "totp") {
return;
}
cipher.name = entry.totp_title;
cipher.notes = entry.totp_notes;
cipher.login.totp = entry.totp_code;
this.importUnmappedFields(cipher, entry, this.TOTP_mappedValues);
}
readonly ENV_VARIABLES_mappedValues = new Set([
"type",
"name",
"environment_variables_title",
"environment_variables_notes",
"environment_variables_variables",
]);
private parseEnvironmentVariables(entry: EnvironmentVariablesEntry, cipher: CipherView) {
if (entry == null || entry.type != "environment_variables") {
return;
}
cipher.type = CipherType.SecureNote;
cipher.secureNote = new SecureNoteView();
cipher.secureNote.type = SecureNoteType.Generic;
cipher.name = entry.environment_variables_title;
cipher.notes = entry.environment_variables_notes;
entry.environment_variables_variables.forEach((KvPair) => {
this.processKvp(cipher, KvPair.key, KvPair.value);
});
this.importUnmappedFields(cipher, entry, this.ENV_VARIABLES_mappedValues);
}
readonly GPG_mappedValues = new Set([
"type",
"name",
"mail_gpg_own_key_title",
"mail_gpg_own_key_public",
"mail_gpg_own_key_name",
"mail_gpg_own_key_email",
"mail_gpg_own_key_private",
]);
private parseGPG(entry: GPGEntry, cipher: CipherView) {
if (entry == null || entry.type != "mail_gpg_own_key") {
return;
}
cipher.type = CipherType.SecureNote;
cipher.secureNote = new SecureNoteView();
cipher.secureNote.type = SecureNoteType.Generic;
cipher.name = entry.mail_gpg_own_key_title;
cipher.notes = entry.mail_gpg_own_key_public;
this.processKvp(cipher, "mail_gpg_own_key_name", entry.mail_gpg_own_key_name);
this.processKvp(cipher, "mail_gpg_own_key_email", entry.mail_gpg_own_key_email);
this.processKvp(
cipher,
"mail_gpg_own_key_private",
entry.mail_gpg_own_key_private,
FieldType.Hidden
);
this.importUnmappedFields(cipher, entry, this.GPG_mappedValues);
}
private importUnmappedFields(
cipher: CipherView,
entry: PsonoItemTypes,
mappedValues: Set<string>
) {
const unmappedFields = Object.keys(entry).filter((x) => !mappedValues.has(x));
unmappedFields.forEach((key) => {
const item = entry as any;
this.processKvp(cipher, key, item[key].toString());
});
}
}

View File

@@ -0,0 +1,109 @@
export type PsonoItemTypes =
| WebsitePasswordEntry
| AppPasswordEntry
| TOTPEntry
| NotesEntry
| EnvironmentVariablesEntry
| GPGEntry
| BookmarkEntry;
export interface PsonoJsonExport {
folders?: FoldersEntity[];
items?: PsonoItemTypes[];
}
export interface FoldersEntity {
name: string;
items: PsonoItemTypes[] | null;
}
export interface RecordBase {
type: PsonoEntryTypes;
name: string;
create_date: string;
write_date: string;
callback_url: string;
callback_user: string;
callback_pass: string;
}
export type PsonoEntryTypes =
| "website_password"
| "bookmark"
| "mail_gpg_own_key"
| "environment_variables"
| "note"
| "application_password"
| "totp";
export interface WebsitePasswordEntry extends RecordBase {
type: "website_password";
autosubmit: boolean;
urlfilter: string;
website_password_title: string;
website_password_url: string;
website_password_username: string;
website_password_password: string;
website_password_notes: string;
website_password_auto_submit: boolean;
website_password_url_filter: string;
}
export interface PsonoEntry {
type: string;
name: string;
}
export interface BookmarkEntry extends RecordBase {
type: "bookmark";
urlfilter: string;
bookmark_title: string;
bookmark_url: string;
bookmark_notes: string;
bookmark_url_filter: string;
}
export interface GPGEntry extends RecordBase {
type: "mail_gpg_own_key";
mail_gpg_own_key_title: string;
mail_gpg_own_key_email: string;
mail_gpg_own_key_name: string;
mail_gpg_own_key_public: string;
mail_gpg_own_key_private: string;
}
export interface EnvironmentVariablesEntry extends RecordBase {
type: "environment_variables";
environment_variables_title: string;
environment_variables_variables: EnvironmentVariables_KVPair[];
environment_variables_notes: string;
}
export interface EnvironmentVariables_KVPair {
key: string;
value: string;
}
export interface AppPasswordEntry extends RecordBase {
type: "application_password";
application_password_title: string;
application_password_username: string;
application_password_password: string;
application_password_notes: string;
}
export interface TOTPEntry extends RecordBase {
type: "totp";
totp_title: string;
totp_period: number;
totp_algorithm: "SHA1";
totp_digits: number;
totp_code: string;
totp_notes: string;
}
export interface NotesEntry extends RecordBase {
type: "note";
note_title: string;
note_notes: string;
}

View File

@@ -59,6 +59,7 @@ import { PasswordBossJsonImporter } from "../importers/passwordboss-json-importe
import { PasswordDragonXmlImporter } from "../importers/passworddragon-xml-importer"; import { PasswordDragonXmlImporter } from "../importers/passworddragon-xml-importer";
import { PasswordSafeXmlImporter } from "../importers/passwordsafe-xml-importer"; import { PasswordSafeXmlImporter } from "../importers/passwordsafe-xml-importer";
import { PasswordWalletTxtImporter } from "../importers/passwordwallet-txt-importer"; import { PasswordWalletTxtImporter } from "../importers/passwordwallet-txt-importer";
import { PsonoJsonImporter } from "../importers/psono/psono-json-importer";
import { RememBearCsvImporter } from "../importers/remembear-csv-importer"; import { RememBearCsvImporter } from "../importers/remembear-csv-importer";
import { RoboFormCsvImporter } from "../importers/roboform-csv-importer"; import { RoboFormCsvImporter } from "../importers/roboform-csv-importer";
import { SafariCsvImporter } from "../importers/safari-csv-importer"; import { SafariCsvImporter } from "../importers/safari-csv-importer";
@@ -280,6 +281,8 @@ export class ImportService implements ImportServiceAbstraction {
return new YotiCsvImporter(); return new YotiCsvImporter();
case "nordpasscsv": case "nordpasscsv":
return new NordPassCsvImporter(); return new NordPassCsvImporter();
case "psonojson":
return new PsonoJsonImporter();
case "passkyjson": case "passkyjson":
return new PasskyJsonImporter(); return new PasskyJsonImporter();
default: default: