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:
committed by
GitHub
parent
651968ca9c
commit
b1a1068906
@@ -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;
|
||||
|
||||
|
||||
281
libs/common/src/importers/psono/psono-json-importer.ts
Normal file
281
libs/common/src/importers/psono/psono-json-importer.ts
Normal 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());
|
||||
});
|
||||
}
|
||||
}
|
||||
109
libs/common/src/importers/psono/psono-json-types.ts
Normal file
109
libs/common/src/importers/psono/psono-json-types.ts
Normal 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;
|
||||
}
|
||||
@@ -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:
|
||||
|
||||
Reference in New Issue
Block a user