diff --git a/libs/importer/src/importers/onepassword/onepassword-1pux-importer.spec.ts b/libs/importer/src/importers/onepassword/onepassword-1pux-importer.spec.ts index 4ec20ba2a87..8dbcf29fd2f 100644 --- a/libs/importer/src/importers/onepassword/onepassword-1pux-importer.spec.ts +++ b/libs/importer/src/importers/onepassword/onepassword-1pux-importer.spec.ts @@ -2,6 +2,7 @@ import { Utils } from "@bitwarden/common/platform/misc/utils"; import { OrganizationId } from "@bitwarden/common/types/guid"; import { FieldType, SecureNoteType, CipherType } from "@bitwarden/common/vault/enums"; import { FieldView } from "@bitwarden/common/vault/models/view/field.view"; +import * as sdkInternal from "@bitwarden/sdk-internal"; import { APICredentialsData } from "../spec-data/onepassword-1pux/api-credentials"; import { BankAccountData } from "../spec-data/onepassword-1pux/bank-account"; @@ -25,11 +26,14 @@ import { SanitizedExport } from "../spec-data/onepassword-1pux/sanitized-export" import { SecureNoteData } from "../spec-data/onepassword-1pux/secure-note"; import { ServerData } from "../spec-data/onepassword-1pux/server"; import { SoftwareLicenseData } from "../spec-data/onepassword-1pux/software-license"; +import { SSH_KeyData } from "../spec-data/onepassword-1pux/ssh-key"; import { SSNData } from "../spec-data/onepassword-1pux/ssn"; import { WirelessRouterData } from "../spec-data/onepassword-1pux/wireless-router"; import { OnePassword1PuxImporter } from "./onepassword-1pux-importer"; +jest.mock("@bitwarden/sdk-internal"); + function validateCustomField(fields: FieldView[], fieldName: string, expectedValue: any) { expect(fields).toBeDefined(); const customField = fields.find((f) => f.name === fieldName); @@ -669,6 +673,37 @@ describe("1Password 1Pux Importer", () => { validateCustomField(cipher.fields, "medication notes", "multiple times a day"); }); + it("should parse category 114 - SSH Key", async () => { + // Mock the SDK import_ssh_key function to return converted OpenSSH format + const mockConvertedKey = { + privateKey: + "-----BEGIN OPENSSH PRIVATE KEY-----\nb3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAAMwAAAAtzc2gtZW\nQyNTUxOQAAACCWsp3FFVVCMGZ23hscRkDPfGzKZ8z1V/ZB9nzbdDFRswAAAJh8F3bYfBd2\n2AAAAAtzc2gtZWQyNTUxOQAAACCWsp3FFVVCMGZ23hscRkDPfGzKZ8z1V/ZB9nzbdDFRsw\nAAAEA59QYE22f+VFHhiyH1Vfqiwz7xLEt1zCuk8M8Ng5LpKpayncUVVUKwZ3beGxxGQM98\nbMpnzPVX9kH2fNt0MVGzAAAAE3Rlc3RAZXhhbXBsZS5jb20BAgMEBQ==\n-----END OPENSSH PRIVATE KEY-----\n", + publicKey: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIJayncUVVUKwZ3beGxxGQM98bMpnzPVX9kH2fNt0MVGz", + fingerprint: "SHA256:/9qSxXuic8kaVBhwv3c8PuetiEpaOgIp7xHNCbcSuN8", + } as sdkInternal.SshKeyView; + + jest.spyOn(sdkInternal, "import_ssh_key").mockReturnValue(mockConvertedKey); + + const importer = new OnePassword1PuxImporter(); + const jsonString = JSON.stringify(SSH_KeyData); + const result = await importer.parse(jsonString); + expect(result != null).toBe(true); + const cipher = result.ciphers.shift(); + expect(cipher.type).toEqual(CipherType.SshKey); + expect(cipher.name).toEqual("Some SSH Key"); + expect(cipher.notes).toEqual("SSH Key Note"); + + // Verify that import_ssh_key was called with the PKCS#8 key from 1Password + expect(sdkInternal.import_ssh_key).toHaveBeenCalledWith( + "-----BEGIN PRIVATE KEY-----\nMFECAQEwBQYDK2VwBCIEIDn1BgTbZ/5UUeGLIfVV+qLBOvEsS3XMK6Twzw2Dkukq\ngSEAlrKdxRVVQrBndt4bHEZAz3xsymfM9Vf2QfZ823QxUbM=\n-----END PRIVATE KEY-----\n", + ); + + // Verify the key was converted to OpenSSH format + expect(cipher.sshKey.privateKey).toEqual(mockConvertedKey.privateKey); + expect(cipher.sshKey.publicKey).toEqual(mockConvertedKey.publicKey); + expect(cipher.sshKey.keyFingerprint).toEqual(mockConvertedKey.fingerprint); + }); + it("should create folders", async () => { const importer = new OnePassword1PuxImporter(); const result = await importer.parse(SanitizedExportJson); diff --git a/libs/importer/src/importers/onepassword/onepassword-1pux-importer.ts b/libs/importer/src/importers/onepassword/onepassword-1pux-importer.ts index 4571a6957c4..48de18bc54b 100644 --- a/libs/importer/src/importers/onepassword/onepassword-1pux-importer.ts +++ b/libs/importer/src/importers/onepassword/onepassword-1pux-importer.ts @@ -8,6 +8,8 @@ import { IdentityView } from "@bitwarden/common/vault/models/view/identity.view" import { LoginView } from "@bitwarden/common/vault/models/view/login.view"; import { PasswordHistoryView } from "@bitwarden/common/vault/models/view/password-history.view"; import { SecureNoteView } from "@bitwarden/common/vault/models/view/secure-note.view"; +import { SshKeyView } from "@bitwarden/common/vault/models/view/ssh-key.view"; +import { import_ssh_key } from "@bitwarden/sdk-internal"; import { ImportResult } from "../../models/import-result"; import { BaseImporter } from "../base-importer"; @@ -80,6 +82,10 @@ export class OnePassword1PuxImporter extends BaseImporter implements Importer { cipher.type = CipherType.Identity; cipher.identity = new IdentityView(); break; + case Category.SSH_Key: + cipher.type = CipherType.SshKey; + cipher.sshKey = new SshKeyView(); + break; default: break; } @@ -316,6 +322,19 @@ export class OnePassword1PuxImporter extends BaseImporter implements Importer { default: break; } + } else if (cipher.type === CipherType.SshKey) { + if (valueKey === "sshKey") { + // Use sshKey.metadata.privateKey instead of the sshKey.privateKey field. + // The sshKey.privateKey field doesn't have a consistent format for every item. + const { privateKey } = field.value.sshKey.metadata; + // Convert SSH key from PKCS#8 (1Password format) to OpenSSH format using SDK + // Note: 1Password does not store password-protected SSH keys, so no password handling needed for now + const parsedKey = import_ssh_key(privateKey); + cipher.sshKey.privateKey = parsedKey.privateKey; + cipher.sshKey.publicKey = parsedKey.publicKey; + cipher.sshKey.keyFingerprint = parsedKey.fingerprint; + return; + } } if (valueKey === "email") { diff --git a/libs/importer/src/importers/onepassword/types/onepassword-1pux-importer-types.ts b/libs/importer/src/importers/onepassword/types/onepassword-1pux-importer-types.ts index 43f3bc4f7d6..a24c6489c24 100644 --- a/libs/importer/src/importers/onepassword/types/onepassword-1pux-importer-types.ts +++ b/libs/importer/src/importers/onepassword/types/onepassword-1pux-importer-types.ts @@ -49,6 +49,7 @@ export const Category = Object.freeze({ EmailAccount: "111", API_Credential: "112", MedicalRecord: "113", + SSH_Key: "114", } as const); /** @@ -133,6 +134,7 @@ export interface Value { creditCardType?: string | null; creditCardNumber?: string | null; reference?: string | null; + sshKey?: SSHKey | null; } export interface Email { @@ -147,6 +149,19 @@ export interface Address { zip: string; state: string; } + +export interface SSHKey { + privateKey: string; + metadata: SSHKeyMetadata; +} + +export interface SSHKeyMetadata { + privateKey: string; + publicKey: string; + fingerprint: string; + keyType: string; +} + export interface InputTraits { keyboard: string; correction: string; diff --git a/libs/importer/src/importers/spec-data/onepassword-1pux/ssh-key.ts b/libs/importer/src/importers/spec-data/onepassword-1pux/ssh-key.ts new file mode 100644 index 00000000000..3e9cde46271 --- /dev/null +++ b/libs/importer/src/importers/spec-data/onepassword-1pux/ssh-key.ts @@ -0,0 +1,83 @@ +import { ExportData } from "../../onepassword/types/onepassword-1pux-importer-types"; + +export const SSH_KeyData: ExportData = { + accounts: [ + { + attrs: { + accountName: "1Password Customer", + name: "1Password Customer", + avatar: "", + email: "username123123123@gmail.com", + uuid: "TRIZ3XV4JJFRXJ3BARILLTUA6E", + domain: "https://my.1password.com/", + }, + vaults: [ + { + attrs: { + uuid: "pqcgbqjxr4tng2hsqt5ffrgwju", + desc: "Just test entries", + avatar: "ke7i5rxnjrh3tj6uesstcosspu.png", + name: "T's Test Vault", + type: "U", + }, + items: [ + { + uuid: "kf7wevmfiqmbgyao42plvgrasy", + favIndex: 0, + createdAt: 1724868152, + updatedAt: 1724868152, + state: "active", + categoryUuid: "114", + details: { + loginFields: [], + notesPlain: "SSH Key Note", + sections: [ + { + title: "SSH Key Section", + fields: [ + { + title: "private key", + id: "private_key", + value: { + sshKey: { + privateKey: + "-----BEGIN PRIVATE KEY-----\nMFECAQEwBQYDK2VwBCIEIDn1BgTbZ/5UUeGLIfVV+qLBOvEsS3XMK6Twzw2Dkukq\ngSEAlrKdxRVVQrBndt4bHEZAz3xsymfM9Vf2QfZ823QxUbM=\n-----END PRIVATE KEY-----\n", + metadata: { + privateKey: + "-----BEGIN PRIVATE KEY-----\nMFECAQEwBQYDK2VwBCIEIDn1BgTbZ/5UUeGLIfVV+qLBOvEsS3XMK6Twzw2Dkukq\ngSEAlrKdxRVVQrBndt4bHEZAz3xsymfM9Vf2QfZ823QxUbM=\n-----END PRIVATE KEY-----\n", + publicKey: + "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIJayncUVVUKwZ3beGxxGQM98bMpnzPVX9kH2fNt0MVGz", + fingerprint: "SHA256:/9qSxXuic8kaVBhwv3c8PuetiEpaOgIp7xHNCbcSuN8", + keyType: "ed25519", + }, + }, + }, + guarded: true, + multiline: false, + dontGenerate: false, + inputTraits: { + keyboard: "default", + correction: "default", + capitalization: "default", + }, + }, + ], + hideAddAnotherField: true, + }, + ], + passwordHistory: [], + }, + overview: { + subtitle: "SHA256:/9qSxXuic8kaVBhwv3c8PuetiEpaOgIp7xHNCbcSuN8", + icons: null, + title: "Some SSH Key", + url: "", + watchtowerExclusions: null, + }, + }, + ], + }, + ], + }, + ], +};