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 1224bc4691c..e3baef5da49 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 00000000000..a156046abf8
--- /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 00000000000..2d2ee3debd9
--- /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 00000000000..f77bb09e11d
--- /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 6535b133636..e6657a92d05 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 00000000000..01a7f0d1cd5
--- /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 00000000000..fb9bf11e515
--- /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 202a899794b..7543ec2bfdd 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;
}