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:
@@ -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);
|
||||
|
||||
@@ -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") {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
};
|
||||
Reference in New Issue
Block a user