1
0
mirror of https://github.com/bitwarden/browser synced 2026-02-06 03:33:30 +00:00

[PM-30894] Support importing SSH keys from 1pux (#18391)

* Support importing SSH keys from 1pux

Co-authored-by: Bernd Schoolmann <mail@quexten.com>
Co-authored-by: Daniel James Smith <djsmith85@users.noreply.github.com>

* Propagate SSH key import error

---------

Co-authored-by: Bernd Schoolmann <mail@quexten.com>
Co-authored-by: Daniel James Smith <djsmith85@users.noreply.github.com>
This commit is contained in:
Sola
2026-02-04 04:18:34 +08:00
committed by GitHub
parent 51a99fecd8
commit eaa7e5ab2a
4 changed files with 152 additions and 0 deletions

View File

@@ -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);

View File

@@ -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") {

View File

@@ -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;

View File

@@ -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,
},
},
],
},
],
},
],
};