From 8c8d4b3e3eadc73b7a3591296980d44a96fa79cd Mon Sep 17 00:00:00 2001 From: Daniel James Smith Date: Mon, 19 Dec 2022 21:47:45 +0100 Subject: [PATCH] Add passky importer (#4253) Create types for passky export format Add test files Write tests for passky-json-importer Write importer for passky export Register 'passkyjson' with `importOptions` Import/register passky-json-importer with import.service Add instructions on how to export from Passky --- .../tools/import-export/import.component.html | 4 ++ .../importers/passky-json-importer.spec.ts | 34 +++++++++++++++ .../passky-json/passky-encrypted.json.ts | 15 +++++++ .../passky-json/passky-unencrypted.json.ts | 13 ++++++ libs/common/src/enums/importOptions.ts | 1 + .../importers/passky/passky-json-importer.ts | 43 +++++++++++++++++++ .../src/importers/passky/passky-json-types.ts | 11 +++++ libs/common/src/services/import.service.ts | 3 ++ 8 files changed, 124 insertions(+) create mode 100644 libs/common/spec/importers/passky-json-importer.spec.ts create mode 100644 libs/common/spec/importers/test-data/passky-json/passky-encrypted.json.ts create mode 100644 libs/common/spec/importers/test-data/passky-json/passky-unencrypted.json.ts create mode 100644 libs/common/src/importers/passky/passky-json-importer.ts create mode 100644 libs/common/src/importers/passky/passky-json-types.ts 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 1224bc4691..e3baef5da4 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 "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/passky-json-importer.spec.ts b/libs/common/spec/importers/passky-json-importer.spec.ts new file mode 100644 index 0000000000..a156046abf --- /dev/null +++ b/libs/common/spec/importers/passky-json-importer.spec.ts @@ -0,0 +1,34 @@ +import { PasskyJsonImporter as Importer } from "@bitwarden/common/importers/passky/passky-json-importer"; + +import { testData as EncryptedData } from "./test-data/passky-json/passky-encrypted.json"; +import { testData as UnencryptedData } from "./test-data/passky-json/passky-unencrypted.json"; + +describe("Passky Json Importer", () => { + let importer: Importer; + beforeEach(() => { + importer = new Importer(); + }); + + it("should not import encrypted backups", async () => { + const testDataJson = JSON.stringify(EncryptedData); + const result = await importer.parse(testDataJson); + expect(result != null).toBe(true); + expect(result.success).toBe(false); + expect(result.errorMessage).toBe("Unable to import an encrypted passky backup."); + }); + + it("should parse login data", async () => { + const testDataJson = JSON.stringify(UnencryptedData); + const result = await importer.parse(testDataJson); + expect(result != null).toBe(true); + + const cipher = result.ciphers.shift(); + expect(cipher.name).toEqual("https://bitwarden.com/"); + expect(cipher.login.username).toEqual("testUser"); + expect(cipher.login.password).toEqual("testPassword"); + expect(cipher.login.uris.length).toEqual(1); + const uriView = cipher.login.uris.shift(); + expect(uriView.uri).toEqual("https://bitwarden.com/"); + expect(cipher.notes).toEqual("my notes"); + }); +}); diff --git a/libs/common/spec/importers/test-data/passky-json/passky-encrypted.json.ts b/libs/common/spec/importers/test-data/passky-json/passky-encrypted.json.ts new file mode 100644 index 0000000000..2d2ee3debd --- /dev/null +++ b/libs/common/spec/importers/test-data/passky-json/passky-encrypted.json.ts @@ -0,0 +1,15 @@ +import { PasskyJsonExport } from "@bitwarden/common/importers/passky/passky-json-types"; + +export const testData: PasskyJsonExport = { + encrypted: true, + passwords: [ + { + website: + "w68uw6nCjUI3w7MNYsK7w6xqwqHDlXLCpsOEw4/Dq8KbIMK3w6fCvQJFFcOECsOlwprCqUAawqnDvsKbwrLCsCXCtcOlw4dp", + username: "bMKyUC0VPTx5woHCr8K9wpvDgGrClFAKw6VfJTgob8KVwqNoN8KIEA==", + password: "XcKxO2FjwqIJPkoHwqrDvcKtXcORw6TDlMOlw7TDvMORfmlNdMKOwq7DocO+", + message: + "w5jCrWTCgAV1RcO+DsOzw5zCvD5CwqLCtcKtw6sPwpbCmcOxwrfDlcOQw4h1wqomEhNtUkRgwrzCkxrClFBSHsO5wrfCrg==", + }, + ], +}; diff --git a/libs/common/spec/importers/test-data/passky-json/passky-unencrypted.json.ts b/libs/common/spec/importers/test-data/passky-json/passky-unencrypted.json.ts new file mode 100644 index 0000000000..f77bb09e11 --- /dev/null +++ b/libs/common/spec/importers/test-data/passky-json/passky-unencrypted.json.ts @@ -0,0 +1,13 @@ +import { PasskyJsonExport } from "@bitwarden/common/importers/passky/passky-json-types"; + +export const testData: PasskyJsonExport = { + encrypted: false, + passwords: [ + { + website: "https://bitwarden.com/", + username: "testUser", + password: "testPassword", + message: "my notes", + }, + ], +}; diff --git a/libs/common/src/enums/importOptions.ts b/libs/common/src/enums/importOptions.ts index 6535b13363..e6657a92d0 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: "passkyjson", name: "Passky (json)" }, ] as const; export type ImportType = diff --git a/libs/common/src/importers/passky/passky-json-importer.ts b/libs/common/src/importers/passky/passky-json-importer.ts new file mode 100644 index 0000000000..01a7f0d1cd --- /dev/null +++ b/libs/common/src/importers/passky/passky-json-importer.ts @@ -0,0 +1,43 @@ +import { ImportResult } from "../../models/domain/import-result"; +import { BaseImporter } from "../base-importer"; +import { Importer } from "../importer"; + +import { PasskyJsonExport } from "./passky-json-types"; + +export class PasskyJsonImporter extends BaseImporter implements Importer { + parse(data: string): Promise { + const result = new ImportResult(); + const passkyExport: PasskyJsonExport = JSON.parse(data); + if ( + passkyExport == null || + passkyExport.passwords == null || + passkyExport.passwords.length === 0 + ) { + result.success = false; + return Promise.resolve(result); + } + + if (passkyExport.encrypted == true) { + result.success = false; + result.errorMessage = "Unable to import an encrypted passky backup."; + return Promise.resolve(result); + } + + passkyExport.passwords.forEach((record) => { + const cipher = this.initLoginCipher(); + cipher.name = record.website; + cipher.login.username = record.username; + cipher.login.password = record.password; + + cipher.login.uris = this.makeUriArray(record.website); + cipher.notes = record.message; + + this.convertToNoteIfNeeded(cipher); + this.cleanupCipher(cipher); + result.ciphers.push(cipher); + }); + + result.success = true; + return Promise.resolve(result); + } +} diff --git a/libs/common/src/importers/passky/passky-json-types.ts b/libs/common/src/importers/passky/passky-json-types.ts new file mode 100644 index 0000000000..fb9bf11e51 --- /dev/null +++ b/libs/common/src/importers/passky/passky-json-types.ts @@ -0,0 +1,11 @@ +export interface PasskyJsonExport { + encrypted: boolean; + passwords: LoginEntry[]; +} + +export interface LoginEntry { + website: string; + username: string; + password: string; + message: string; +} diff --git a/libs/common/src/services/import.service.ts b/libs/common/src/services/import.service.ts index 202a899794..7543ec2bfd 100644 --- a/libs/common/src/services/import.service.ts +++ b/libs/common/src/services/import.service.ts @@ -51,6 +51,7 @@ import { OnePasswordMacCsvImporter } from "../importers/onepassword/onepassword- import { OnePasswordWinCsvImporter } from "../importers/onepassword/onepassword-win-csv-importer"; import { PadlockCsvImporter } from "../importers/padlock-csv-importer"; import { PassKeepCsvImporter } from "../importers/passkeep-csv-importer"; +import { PasskyJsonImporter } from "../importers/passky/passky-json-importer"; import { PassmanJsonImporter } from "../importers/passman-json-importer"; import { PasspackCsvImporter } from "../importers/passpack-csv-importer"; import { PasswordAgentCsvImporter } from "../importers/passwordagent-csv-importer"; @@ -279,6 +280,8 @@ export class ImportService implements ImportServiceAbstraction { return new YotiCsvImporter(); case "nordpasscsv": return new NordPassCsvImporter(); + case "passkyjson": + return new PasskyJsonImporter(); default: return null; }