diff --git a/apps/web/src/app/tools/import-export/import.component.html b/apps/web/src/app/tools/import-export/import.component.html
index e3baef5da49..8911a63306f 100644
--- a/apps/web/src/app/tools/import-export/import.component.html
+++ b/apps/web/src/app/tools/import-export/import.component.html
@@ -288,6 +288,10 @@
From the Yoti browser extension, click on "Settings", then "Export Saved Logins" and save the
CSV file.
+
+ 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.
+
Log in to "https://vault.passky.org" → "Import & Export" → "Export" in the Passky
section. ("Backup" is unsupported as it is encrypted).
diff --git a/libs/common/spec/importers/psono-json-importer.spec.ts b/libs/common/spec/importers/psono-json-importer.spec.ts
new file mode 100644
index 00000000000..f9fc5f8c7fe
--- /dev/null
+++ b/libs/common/spec/importers/psono-json-importer.spec.ts
@@ -0,0 +1,228 @@
+import { CipherType } from "@bitwarden/common/enums/cipherType";
+import { FieldType } from "@bitwarden/common/enums/fieldType";
+import { PsonoJsonImporter as Importer } from "@bitwarden/common/importers/psono/psono-json-importer";
+import { FieldView } from "@bitwarden/common/models/view/field.view";
+
+import { ApplicationPasswordsData } from "./test-data/psono-json/application-passwords";
+import { BookmarkData } from "./test-data/psono-json/bookmark.json";
+import { EmptyTestFolderData } from "./test-data/psono-json/empty-folders";
+import { EnvVariablesData } from "./test-data/psono-json/environment-variables";
+import { FoldersTestData } from "./test-data/psono-json/folders";
+import { GPGData } from "./test-data/psono-json/gpg";
+import { NotesData } from "./test-data/psono-json/notes";
+import { TOTPData } from "./test-data/psono-json/totp";
+import { WebsiteLoginsData } from "./test-data/psono-json/website-logins";
+
+function validateCustomField(
+ fields: FieldView[],
+ fieldName: string,
+ expectedValue: any,
+ fieldType = FieldType.Text
+) {
+ expect(fields).not.toBeUndefined();
+ const customField = fields.find((f) => f.name === fieldName);
+ expect(customField).not.toBeNull();
+
+ expect(customField.value).toEqual(expectedValue);
+ expect(customField.type).toEqual(fieldType);
+}
+
+describe("PSONO JSON Importer", () => {
+ const WebsiteLoginsDataJson = JSON.stringify(WebsiteLoginsData);
+ const ApplicationPasswordsDataJson = JSON.stringify(ApplicationPasswordsData);
+ const BookmarkDataJson = JSON.stringify(BookmarkData);
+ const NotesDataJson = JSON.stringify(NotesData);
+ const TOTPDataJson = JSON.stringify(TOTPData);
+ const EmptyTestFolderDataJson = JSON.stringify(EmptyTestFolderData);
+ const FoldersTestDataJson = JSON.stringify(FoldersTestData);
+ const GPGDataJson = JSON.stringify(GPGData);
+ const EnvVariablesDataJson = JSON.stringify(EnvVariablesData);
+
+ it("should parse Website/Password data", async () => {
+ const importer = new Importer();
+ const result = await importer.parse(WebsiteLoginsDataJson);
+ expect(result != null).toBe(true);
+
+ const cipher = result.ciphers.shift();
+
+ expect(cipher.type).toEqual(CipherType.Login);
+ expect(cipher.name).toEqual("TestEntry");
+ expect(cipher.notes).toEqual("some notes");
+
+ expect(cipher.login.username).toEqual("testUser");
+ expect(cipher.login.password).toEqual("testPassword");
+ expect(cipher.login.uris.length).toEqual(1);
+ expect(cipher.login.uri).toEqual("http://bitwarden.com");
+
+ expect(cipher.fields.length).toBe(7);
+ validateCustomField(cipher.fields, "website_password_auto_submit", "true", FieldType.Boolean);
+ validateCustomField(cipher.fields, "website_password_url_filter", "filter");
+ validateCustomField(cipher.fields, "create_date", "2022-12-13T19:24:09.810266Z");
+ validateCustomField(cipher.fields, "write_date", "2022-12-13T19:24:09.810292Z");
+ validateCustomField(cipher.fields, "callback_url", "callback");
+ validateCustomField(cipher.fields, "callback_user", "callbackUser");
+ validateCustomField(cipher.fields, "callback_pass", "callbackPassword");
+ });
+
+ it("should parse Application Password data", async () => {
+ const importer = new Importer();
+ const result = await importer.parse(ApplicationPasswordsDataJson);
+ expect(result != null).toBe(true);
+
+ const cipher = result.ciphers.shift();
+
+ expect(cipher.type).toEqual(CipherType.Login);
+ expect(cipher.name).toEqual("My App Password");
+ expect(cipher.notes).toEqual("some notes for the APP");
+
+ expect(cipher.login.username).toEqual("someUser");
+ expect(cipher.login.password).toEqual("somePassword");
+
+ expect(cipher.fields.length).toBe(2);
+ validateCustomField(cipher.fields, "create_date", "2022-12-13T19:42:05.784077Z");
+ validateCustomField(cipher.fields, "write_date", "2022-12-13T19:42:05.784103Z");
+ });
+
+ it("should parse bookmark data", async () => {
+ const importer = new Importer();
+ const result = await importer.parse(BookmarkDataJson);
+ expect(result != null).toBe(true);
+
+ const cipher = result.ciphers.shift();
+
+ expect(cipher.type).toEqual(CipherType.Login);
+ expect(cipher.name).toEqual("MyBookmark");
+ expect(cipher.notes).toEqual("my notes for bitwarden.com");
+
+ expect(cipher.login.uris.length).toEqual(1);
+ expect(cipher.login.uri).toEqual("https://bitwarden.com");
+
+ expect(cipher.fields.length).toBe(4);
+ validateCustomField(cipher.fields, "create_date", "2022-12-13T19:39:26.631530Z");
+ validateCustomField(cipher.fields, "write_date", "2022-12-13T19:39:26.631553Z");
+ });
+
+ it("should parse notes", async () => {
+ const importer = new Importer();
+ const result = await importer.parse(NotesDataJson);
+ expect(result != null).toBe(true);
+
+ const cipher = result.ciphers.shift();
+ expect(cipher.type).toEqual(CipherType.SecureNote);
+ expect(cipher.name).toEqual("My Note");
+ expect(cipher.notes).toEqual("Notes for my Note");
+
+ expect(cipher.fields.length).toBe(2);
+ validateCustomField(cipher.fields, "create_date", "2022-12-13T19:41:18.770714Z");
+ validateCustomField(cipher.fields, "write_date", "2022-12-13T19:41:18.770738Z");
+ });
+
+ it("should parse TOTP", async () => {
+ const importer = new Importer();
+ const result = await importer.parse(TOTPDataJson);
+ expect(result != null).toBe(true);
+
+ const cipher = result.ciphers.shift();
+ expect(cipher.type).toEqual(CipherType.Login);
+ expect(cipher.name).toEqual("My TOTP");
+ expect(cipher.notes).toEqual("Notes for TOTP");
+
+ expect(cipher.login.totp).toEqual("someSecretOfMine");
+
+ expect(cipher.fields.length).toBe(5);
+ validateCustomField(cipher.fields, "totp_period", "30", FieldType.Text);
+ validateCustomField(cipher.fields, "totp_algorithm", "SHA1", FieldType.Text);
+ validateCustomField(cipher.fields, "totp_digits", "6", FieldType.Text);
+ validateCustomField(cipher.fields, "create_date", "2022-12-13T19:41:42.972586Z");
+ validateCustomField(cipher.fields, "write_date", "2022-12-13T19:41:42.972609Z");
+ });
+
+ // Skipping this test until we can save GPG into notes/custom fields
+ it.skip("should parse GPG data", async () => {
+ const importer = new Importer();
+ const result = await importer.parse(GPGDataJson);
+ expect(result != null).toBe(true);
+
+ const cipher = result.ciphers.shift();
+ expect(cipher.type).toEqual(CipherType.SecureNote);
+ expect(cipher.name).toEqual("my test gpg key");
+ //Public Key
+ expect(cipher.notes).toEqual(
+ "-----BEGIN PGP PUBLIC KEY BLOCK-----\n\nxsFNBGOY1RUBEAC5RfpxtP9I9OQAuqiLLyuVrf26SvsETtP6FSDj3WK9ozjv\nXjTW7cH8hQ3ckMwFJmFK3YYOf9LWwrhUV/6oqvO3WSwmaJSt9sOYCegWDGSP\nFU7aItnt5Z5+a5nzXAS82LgVVCn/E5OMWY2fS24wUoith6XFbUbluQK+N28K\nT7n2RHQ0Ai+e6i4lPPTJVoy1yy34+xDeHbAbx5kzcWqycImGyEBE0iiGnPB8\nL/gOtq4+QwLmFpmRAGERs82y2WbR/PfRneL4OLtSeDOaW6pQdPBGmBiuvwa4\n2YgGhUBkrlJg4wPb/dfsS0+4yF00NTxnBIUjkh3ZWisDBlODK3dwzkMQVI9Y\n26g+Gm3FmhXPBydQC/3FIAIADnWz1yfF0yMX1xQ9mFLD1sAk58WqFjt7X/v8\nSKZETdBJEx1x5TIKjbMgCcUqS3lrHWt2OsaSslEOONxONtRJ+5WIjQDz2x/r\nEgLQ5GA+djTrQqX75/kfolcC0lmIBibDf2CSNf/Adt99NZr8xmBqrS/FXhVm\n/kFU+V6NAKyixEw36UwfA2+Jb4LoKPfWPvRQPbQ3y3VhiGLWa4I7Fl8/IR8c\nG99HMc7fbB55dJ4idl9eOrh8QjChLGP4cMeoxGFHjjhqLt0xvuwj4GRY3PQj\ndfvuwM4D/fT7iyMypUCxZNeRgDduq7ViLyfY/wARAQABzSVNeSBuYW1lIDxk\nanNtaXRoLmJpdHdhcmRlbkBnbWFpbC5jb20+wsGKBBABCAAdBQJjmNUVBAsJ\nBwgDFQgKBBYAAgECGQECGwMCHgEAIQkQOBCqNBFFpAIWIQReRzYN72bBD4qn\nxvM4EKo0EUWkAuQkD/9V2IgPBTBILXdpFP4s+0jqJVBCQ/JcMG6Ce7Qhdk2U\nCF6wEf0I/Gc5Ei1+d+lpkIYVquvHdCKUJZSJ+qL8MjnIDumHLQzu8xxqmPcQ\nlASTpobtcAdpLY7u+Z7QnQmUQN+ITLG1pg3dlgveDkSuRa5R9gxS2pEduXKg\n2lCumvhFe3AdJzit+HqGk4VkdCq51+jEsGpAH1BpX3yJbajuxBbl0PeU5lyq\nMcKr/k+yem4Kv/qPjhj9VawSpgSBQZbgffvF5Pzbf7mRzQkV1qdRPKkRiLXF\nW8tqQcwUbDwKuxyhHyZ9aH3bvcP+djud+3B0rlbHtvv4H3S/g7dXqlqCRqaC\niqW1SDqPSfA40Ill2+84veF6K2EkrOTvKjAaRvh50ZkXAadpQTh0jMX96Yyl\n0zdvIx4U5StI/actT/IZUULJ7RE7TqOmQEZ6sSRNtyFJWxF4XnbiiLnYbi8B\n02HozV3CKGoJlGGe4lURHeH/H3mHTU7AeiWr4hfUcybA7AJDVTxzTV7UXEeL\nnjvSc/dSqFkTuBjOAlUn8cmYq/n7Pe2b8fUdUpYKan/4KjAxsKF0Uj+S96qh\nv+XFZIa+905Y0x+hrsuVfIE3LmlYovJ0vUdZFtjmW0EqBe8Z8eE1BB9RzZe8\nVR7jHiXzJd8MuRBbiq0bLJ90adsIHHr6Kk6yX3gtJc7BTQRjmNUVARAAwogX\ne67l5i+4sLXreAmsIkB+PvsDMHMtTkJMMoQEw4MD5znOb033J3/DMbt3zzUw\nJV3O5CWJm5zsx45oyb6wI2mOdtx+4nYoHF8av/z1PtIlLsypvkwMfqOYCt19\n+cIoV6aKkqAGETaIYQdxNw3S5uvribePH02ZorWW0q2MbZrqmOpA7p09ze6s\nKb/X4kMQFbEkEdg+d1e+HLAxKOxa88fvMEQtfTiYdKFiZmEvWAen0Xc7KwCW\nw9j/S8phXnAghwAERTlhcWcQIP9WajAx0/UbktxzoKqvIfrT6V82v35Eiblr\nmzMKzMvoH5AzHFE1G6EA0PZ9G5U8Ov9q64eUe+2+OtoB4nACR5mw3QqzDgPz\nt30lb+aoxHfGC+U2AWGcNTnAj8heeyWrkQ4rtXqIvvV/7P8Yddt1nmli7X+e\ngIuOiIoPoadwAhAwDJ42RnbjKqbsWNWXnL0j+3b0yVxPASZbd4pfTDLV+kUK\neC9kLv1IwD5NZiDXgXxZCMnXbqzagp706xQFXBPXqgWqKSO5NljH5YbFdtiK\nFYTGYvBP6Az/xwtEc5pFs/I+8UeksfjQONCYRikYYzbc7mciZ48TZWd1RR2+\nxoxZZ8QTP+dSbQfu7cTdgpdVLYrvjAdLhb8mD4GGMxQAtg5dWULN3VQd2awi\njunUwYAQLRwY9YvDMa8AEQEAAcLBdgQYAQgACQUCY5jVFQIbDAAhCRA4EKo0\nEUWkAhYhBF5HNg3vZsEPiqfG8zgQqjQRRaQCPFgP/1fVPkyXRz5v3rGjq49F\njOkJcsIXe+LbNIgMMVqHDx7Rnwcmo7x12qygLjT2oj/jeMDYt4yYNaR05ajn\nvpdK4acT60J/hnS5RHm3jWtQuGpwmvkAdrpkb06/WtoIsSvaZ2Jv107gkA2E\nhDsilZhE9sRN3ltSOXzaoWoWLh3i0dnxKJal3/05eOsJNs9iunHVPY6T5Nhy\nuBtFsdhdO5PoDKf0+/cX4HnjxP5aW0s6WiSU28rzMquFOxwHYqqsjTcRrr8C\nY83CcxVcV5o+iNcDMuFMG7FXzrHiI4EVV6G3SBlq5OZ/skkiFey3kB5uMpuM\nGddJwWkgFw0owY/XyyMVoeR65uXVIafedyEQw00uM7BDON+utNN811oKeWSc\nmLlFXWwwOK8434LVYpPedeiPyFk0YNcRfZC4z180xDxDGX+KilkNhf+0yvbT\nVRM9LDuvu+YRKWhmy1PLsbIqcrcCYI0bGNYPd7bzw2n0qQx47H/IIy9wUFrw\n7RqN/WMjAzQ0aJLx26VblljnFFSNIix4ca41+lIVOrLhEVNv4mZheyCxFFGa\nwhuX357ntRS19KYfezrf36XwFym3nTXTSxhjJxlNjBBM4pojxYjjXnmjI4L0\nKG2iJSdD/9+qnU7tH+l0Np079WopGjB6A21kvBIiESPsS3S56gG66ZkbG/ZC\nAZTY\n=kfi8\n-----END PGP PUBLIC KEY BLOCK-----\n"
+ );
+
+ expect(cipher.fields.length).toBe(5);
+ //Keyname
+ validateCustomField(cipher.fields, "mail_gpg_own_key_name", "My key name");
+ //Email
+ validateCustomField(cipher.fields, "mail_gpg_own_key_email", "some@email.com");
+ // Private Key
+ validateCustomField(
+ cipher.fields,
+ "mail_gpg_own_key_private",
+ "-----BEGIN PGP PRIVATE KEY BLOCK-----\n\n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\nEiIRI+xLdLnqAbrpmRsb9kIBlNg=\n=+NhB\n-----END PGP PRIVATE KEY BLOCK-----\n",
+ FieldType.Hidden
+ );
+ validateCustomField(cipher.fields, "create_date", "2022-12-13T19:40:16.070379Z");
+ validateCustomField(cipher.fields, "write_date", "2022-12-13T19:40:16.070404Z");
+ });
+
+ it("should parse Environment variables data", async () => {
+ const importer = new Importer();
+ const result = await importer.parse(EnvVariablesDataJson);
+ expect(result != null).toBe(true);
+
+ const cipher = result.ciphers.shift();
+ expect(cipher.type).toEqual(CipherType.SecureNote);
+ expect(cipher.name).toEqual("My Environment Variables");
+ expect(cipher.notes).toBe("Notes for environment variables");
+
+ expect(cipher.fields.length).toBe(4);
+ validateCustomField(cipher.fields, "Key1", "Value1");
+ validateCustomField(cipher.fields, "Key2", "Value2");
+ validateCustomField(cipher.fields, "create_date", "2022-12-13T19:41:02.028884Z");
+ validateCustomField(cipher.fields, "write_date", "2022-12-13T19:41:02.028909Z");
+ });
+
+ it("should not create empty folders", async () => {
+ const importer = new Importer();
+ const result = await importer.parse(EmptyTestFolderDataJson);
+ expect(result != null).toBe(true);
+
+ expect(result.folders.length).toBe(0);
+ });
+
+ it("should create folders", async () => {
+ const importer = new Importer();
+ const result = await importer.parse(FoldersTestDataJson);
+ expect(result != null).toBe(true);
+
+ const folders = result.folders;
+ expect(folders.length).toBe(2);
+ expect(folders[0].name).toBe("TestFolder");
+ expect(folders[1].name).toBe("TestFolder2");
+ });
+
+ it("should assign entries to folders", async () => {
+ const importer = new Importer();
+ const result = await importer.parse(FoldersTestDataJson);
+ expect(result != null).toBe(true);
+
+ const folders = result.folders;
+ // // Check that ciphers have a folder assigned to them
+ expect(result.ciphers.filter((c) => c.folderId === folders[0].id).length).toBeGreaterThan(0);
+ expect(result.ciphers.filter((c) => c.folderId === folders[1].id).length).toBeGreaterThan(0);
+ });
+
+ it("should create collections if part of an organization", async () => {
+ const importer = new Importer();
+ importer.organizationId = "someOrg";
+ const result = await importer.parse(FoldersTestDataJson);
+ expect(result != null).toBe(true);
+
+ const collections = result.collections;
+ expect(collections.length).toBe(2);
+ expect(collections[0].name).toBe("TestFolder");
+ expect(collections[1].name).toBe("TestFolder2");
+ });
+});
diff --git a/libs/common/spec/importers/test-data/psono-json/application-passwords.ts b/libs/common/spec/importers/test-data/psono-json/application-passwords.ts
new file mode 100644
index 00000000000..df9416dd52d
--- /dev/null
+++ b/libs/common/spec/importers/test-data/psono-json/application-passwords.ts
@@ -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: "",
+ },
+ ],
+};
diff --git a/libs/common/spec/importers/test-data/psono-json/bookmark.json.ts b/libs/common/spec/importers/test-data/psono-json/bookmark.json.ts
new file mode 100644
index 00000000000..a4870b8d0ab
--- /dev/null
+++ b/libs/common/spec/importers/test-data/psono-json/bookmark.json.ts
@@ -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: "",
+ },
+ ],
+};
diff --git a/libs/common/spec/importers/test-data/psono-json/empty-folders.ts b/libs/common/spec/importers/test-data/psono-json/empty-folders.ts
new file mode 100644
index 00000000000..e775113be20
--- /dev/null
+++ b/libs/common/spec/importers/test-data/psono-json/empty-folders.ts
@@ -0,0 +1,10 @@
+import { PsonoJsonExport } from "@bitwarden/common/importers/psono/psono-json-types";
+
+export const EmptyTestFolderData: PsonoJsonExport = {
+ folders: [
+ {
+ name: "EmptyFolder",
+ items: [],
+ },
+ ],
+};
diff --git a/libs/common/spec/importers/test-data/psono-json/environment-variables.ts b/libs/common/spec/importers/test-data/psono-json/environment-variables.ts
new file mode 100644
index 00000000000..3bc6de30bde
--- /dev/null
+++ b/libs/common/spec/importers/test-data/psono-json/environment-variables.ts
@@ -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: "",
+ },
+ ],
+};
diff --git a/libs/common/spec/importers/test-data/psono-json/folders.ts b/libs/common/spec/importers/test-data/psono-json/folders.ts
new file mode 100644
index 00000000000..3968f714d86
--- /dev/null
+++ b/libs/common/spec/importers/test-data/psono-json/folders.ts
@@ -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: [],
+};
diff --git a/libs/common/spec/importers/test-data/psono-json/gpg.ts b/libs/common/spec/importers/test-data/psono-json/gpg.ts
new file mode 100644
index 00000000000..177f4ef762d
--- /dev/null
+++ b/libs/common/spec/importers/test-data/psono-json/gpg.ts
@@ -0,0 +1,23 @@
+import { PsonoJsonExport } from "@bitwarden/common/importers/psono/psono-json-types";
+
+export const GPGData: PsonoJsonExport = {
+ folders: [],
+ items: [
+ {
+ type: "mail_gpg_own_key",
+ name: "my test gpg key",
+ mail_gpg_own_key_title: "my test gpg key",
+ mail_gpg_own_key_email: "some@email.com",
+ mail_gpg_own_key_name: "My key name",
+ mail_gpg_own_key_public:
+ "-----BEGIN PGP PUBLIC KEY BLOCK-----\n\n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\nAZTY\n=kfi8\n-----END PGP PUBLIC KEY BLOCK-----\n",
+ mail_gpg_own_key_private:
+ "-----BEGIN PGP PRIVATE KEY BLOCK-----\n\n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\nEiIRI+xLdLnqAbrpmRsb9kIBlNg=\n=+NhB\n-----END PGP PRIVATE KEY BLOCK-----\n",
+ create_date: "2022-12-13T19:40:16.070379Z",
+ write_date: "2022-12-13T19:40:16.070404Z",
+ callback_url: "",
+ callback_user: "",
+ callback_pass: "",
+ },
+ ],
+};
diff --git a/libs/common/spec/importers/test-data/psono-json/notes.ts b/libs/common/spec/importers/test-data/psono-json/notes.ts
new file mode 100644
index 00000000000..c5290afdad4
--- /dev/null
+++ b/libs/common/spec/importers/test-data/psono-json/notes.ts
@@ -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: "",
+ },
+ ],
+};
diff --git a/libs/common/spec/importers/test-data/psono-json/totp.ts b/libs/common/spec/importers/test-data/psono-json/totp.ts
new file mode 100644
index 00000000000..8c903437996
--- /dev/null
+++ b/libs/common/spec/importers/test-data/psono-json/totp.ts
@@ -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: "",
+ },
+ ],
+};
diff --git a/libs/common/spec/importers/test-data/psono-json/website-logins.ts b/libs/common/spec/importers/test-data/psono-json/website-logins.ts
new file mode 100644
index 00000000000..3dd2320b23f
--- /dev/null
+++ b/libs/common/spec/importers/test-data/psono-json/website-logins.ts
@@ -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",
+ },
+ ],
+};
diff --git a/libs/common/src/enums/importOptions.ts b/libs/common/src/enums/importOptions.ts
index e6657a92d05..cd9a5bc01c7 100644
--- a/libs/common/src/enums/importOptions.ts
+++ b/libs/common/src/enums/importOptions.ts
@@ -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;
diff --git a/libs/common/src/importers/psono/psono-json-importer.ts b/libs/common/src/importers/psono/psono-json-importer.ts
new file mode 100644
index 00000000000..0e173b131d2
--- /dev/null
+++ b/libs/common/src/importers/psono/psono-json-importer.ts
@@ -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 {
+ 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
+ ) {
+ 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());
+ });
+ }
+}
diff --git a/libs/common/src/importers/psono/psono-json-types.ts b/libs/common/src/importers/psono/psono-json-types.ts
new file mode 100644
index 00000000000..bc15f4f3ec0
--- /dev/null
+++ b/libs/common/src/importers/psono/psono-json-types.ts
@@ -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;
+}
diff --git a/libs/common/src/services/import.service.ts b/libs/common/src/services/import.service.ts
index eec8edf6c56..d7c8cd12375 100644
--- a/libs/common/src/services/import.service.ts
+++ b/libs/common/src/services/import.service.ts
@@ -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: