1
0
mirror of https://github.com/bitwarden/browser.git synced 2024-11-22 11:45:59 +01: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
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
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
CSV file.
</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'">
Log in to "https://vault.passky.org" &rarr; "Import & Export" &rarr; "Export" in the Passky
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: "yoticsv", name: "Yoti (csv)" },
{ id: "nordpasscsv", name: "Nordpass (csv)" },
{ id: "psonojson", name: "Psono (json)" },
{ id: "passkyjson", name: "Passky (json)" },
] 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 { PasswordSafeXmlImporter } from "../importers/passwordsafe-xml-importer";
import { PasswordWalletTxtImporter } from "../importers/passwordwallet-txt-importer";
import { PsonoJsonImporter } from "../importers/psono/psono-json-importer";
import { RememBearCsvImporter } from "../importers/remembear-csv-importer";
import { RoboFormCsvImporter } from "../importers/roboform-csv-importer";
import { SafariCsvImporter } from "../importers/safari-csv-importer";
@ -280,6 +281,8 @@ export class ImportService implements ImportServiceAbstraction {
return new YotiCsvImporter();
case "nordpasscsv":
return new NordPassCsvImporter();
case "psonojson":
return new PsonoJsonImporter();
case "passkyjson":
return new PasskyJsonImporter();
default: