diff --git a/src/backend/database/routes/credentials.ts b/src/backend/database/routes/credentials.ts index 91a2549f..f628da66 100644 --- a/src/backend/database/routes/credentials.ts +++ b/src/backend/database/routes/credentials.ts @@ -6,6 +6,49 @@ import type { Request, Response, NextFunction } from "express"; import jwt from "jsonwebtoken"; import { authLogger } from "../../utils/logger.js"; import { parseSSHKey, parsePublicKey, detectKeyType, validateKeyPair } from "../../utils/ssh-key-utils.js"; +import crypto from "crypto"; +import ssh2Pkg from "ssh2"; +const { utils: ssh2Utils } = ssh2Pkg; + +// Direct SSH key generation with ssh2 - the right way +function generateSSHKeyPair(keyType: string, keySize?: number): { success: boolean; privateKey?: string; publicKey?: string; error?: string } { + console.log('Generating SSH key pair with ssh2:', keyType); + + try { + // Convert our keyType to ssh2 format + let ssh2Type = keyType; + const options: any = {}; + + if (keyType === 'ssh-rsa') { + ssh2Type = 'rsa'; + options.bits = keySize || 2048; + } else if (keyType === 'ssh-ed25519') { + ssh2Type = 'ed25519'; + } else if (keyType === 'ecdsa-sha2-nistp256') { + ssh2Type = 'ecdsa'; + options.bits = 256; // ECDSA P-256 uses 256 bits + } + + // Use ssh2's native key generation + const keyPair = ssh2Utils.generateKeyPairSync(ssh2Type as any, options); + + console.log('SSH key pair generated successfully!'); + console.log('Private key length:', keyPair.private.length); + console.log('Public key preview:', keyPair.public.substring(0, 50) + '...'); + + return { + success: true, + privateKey: keyPair.private, + publicKey: keyPair.public + }; + } catch (error) { + console.error('SSH key generation failed:', error); + return { + success: false, + error: error instanceof Error ? error.message : 'SSH key generation failed' + }; + } +} const router = express.Router(); @@ -828,6 +871,51 @@ router.post("/validate-key-pair", authenticateJWT, async (req: Request, res: Res } }); +// Generate new SSH key pair endpoint +// POST /credentials/generate-key-pair +router.post("/generate-key-pair", authenticateJWT, async (req: Request, res: Response) => { + const { keyType = 'ssh-ed25519', keySize = 2048, passphrase } = req.body; + + console.log("=== Generate Key Pair API Called ==="); + console.log("Key type:", keyType); + console.log("Key size:", keySize); + console.log("Has passphrase:", !!passphrase); + + try { + // Generate keys with crypto, convert public key to SSH format + const result = generateSSHKeyPair(keyType, keySize); + + if (result.success && result.privateKey && result.publicKey) { + const response = { + success: true, + privateKey: result.privateKey, + publicKey: result.publicKey, + keyType: keyType, + format: 'ssh', + algorithm: keyType, + keySize: keyType === 'ssh-rsa' ? keySize : undefined, + curve: keyType === 'ecdsa-sha2-nistp256' ? 'nistp256' : undefined + }; + + console.log("SSH key pair generated successfully:", keyType); + res.json(response); + } else { + console.error("SSH key generation failed:", result.error); + res.status(500).json({ + success: false, + error: result.error || "Failed to generate SSH key pair" + }); + } + } catch (error) { + console.error("Exception in generate-key-pair endpoint:", error); + authLogger.error("Failed to generate key pair", error); + res.status(500).json({ + success: false, + error: error instanceof Error ? error.message : "Failed to generate key pair" + }); + } +}); + // Generate public key from private key endpoint // POST /credentials/generate-public-key router.post("/generate-public-key", authenticateJWT, async (req: Request, res: Response) => { @@ -844,31 +932,175 @@ router.post("/generate-public-key", authenticateJWT, async (req: Request, res: R } try { - console.log("Calling parseSSHKey to generate public key..."); - const keyInfo = parseSSHKey(privateKey, keyPassword); - console.log("parseSSHKey result:", keyInfo); + console.log("Using Node.js crypto to generate public key from private key..."); + console.log("Private key length:", privateKey.length); + console.log("Private key first 100 chars:", privateKey.substring(0, 100)); - if (!keyInfo.success) { + // First try to create private key object from the input + let privateKeyObj; + let parseAttempts = []; + + // Attempt 1: Direct parsing with passphrase + try { + privateKeyObj = crypto.createPrivateKey({ + key: privateKey, + passphrase: keyPassword + }); + console.log("Successfully parsed with passphrase method"); + } catch (error) { + parseAttempts.push(`Method 1 (with passphrase): ${error.message}`); + } + + // Attempt 2: Direct parsing without passphrase + if (!privateKeyObj) { + try { + privateKeyObj = crypto.createPrivateKey(privateKey); + console.log("Successfully parsed without passphrase"); + } catch (error) { + parseAttempts.push(`Method 2 (without passphrase): ${error.message}`); + } + } + + // Attempt 3: Try with explicit format specification + if (!privateKeyObj) { + try { + privateKeyObj = crypto.createPrivateKey({ + key: privateKey, + format: 'pem', + type: 'pkcs8' + }); + console.log("Successfully parsed as PKCS#8"); + } catch (error) { + parseAttempts.push(`Method 3 (PKCS#8): ${error.message}`); + } + } + + // Attempt 4: Try as PKCS#1 RSA + if (!privateKeyObj && privateKey.includes('-----BEGIN RSA PRIVATE KEY-----')) { + try { + privateKeyObj = crypto.createPrivateKey({ + key: privateKey, + format: 'pem', + type: 'pkcs1' + }); + console.log("Successfully parsed as PKCS#1 RSA"); + } catch (error) { + parseAttempts.push(`Method 4 (PKCS#1): ${error.message}`); + } + } + + // Attempt 5: Try as SEC1 EC + if (!privateKeyObj && privateKey.includes('-----BEGIN EC PRIVATE KEY-----')) { + try { + privateKeyObj = crypto.createPrivateKey({ + key: privateKey, + format: 'pem', + type: 'sec1' + }); + console.log("Successfully parsed as SEC1 EC"); + } catch (error) { + parseAttempts.push(`Method 5 (SEC1): ${error.message}`); + } + } + + // Final attempt: Try using ssh2 as fallback + if (!privateKeyObj) { + console.log("Attempting fallback to parseSSHKey function..."); + try { + const keyInfo = parseSSHKey(privateKey, keyPassword); + console.log("parseSSHKey fallback result:", keyInfo); + + if (keyInfo.success && keyInfo.publicKey) { + // Ensure SSH2 fallback also returns proper string + const publicKeyString = String(keyInfo.publicKey); + console.log("SSH2 fallback public key type:", typeof publicKeyString); + console.log("SSH2 fallback public key length:", publicKeyString.length); + + return res.json({ + success: true, + publicKey: publicKeyString, + keyType: keyInfo.keyType + }); + } else { + parseAttempts.push(`SSH2 fallback: ${keyInfo.error || 'No public key generated'}`); + } + } catch (error) { + parseAttempts.push(`SSH2 fallback exception: ${error.message}`); + } + } + + if (!privateKeyObj) { + console.error("All parsing attempts failed:", parseAttempts); return res.status(400).json({ success: false, - error: keyInfo.error || "Failed to parse private key" + error: "Unable to parse private key. Tried multiple formats.", + details: parseAttempts }); } - if (!keyInfo.publicKey || !keyInfo.publicKey.trim()) { - return res.status(400).json({ - success: false, - error: "Unable to generate public key from the provided private key" - }); + // Generate public key from private key + const publicKeyObj = crypto.createPublicKey(privateKeyObj); + const publicKeyPem = publicKeyObj.export({ + type: 'spki', + format: 'pem' + }); + + // Debug: Check what we're actually generating + console.log("Generated public key type:", typeof publicKeyPem); + console.log("Generated public key is Buffer:", Buffer.isBuffer(publicKeyPem)); + + // Ensure publicKeyPem is a string + const publicKeyString = typeof publicKeyPem === 'string' ? publicKeyPem : publicKeyPem.toString('utf8'); + + console.log("Public key string length:", publicKeyString.length); + console.log("Generated public key first 100 chars:", publicKeyString.substring(0, 100)); + console.log("Public key is string:", typeof publicKeyString === 'string'); + console.log("Public key contains PEM header:", publicKeyString.includes('-----BEGIN PUBLIC KEY-----')); + + // Detect key type from the private key object + let keyType = 'unknown'; + const asymmetricKeyType = privateKeyObj.asymmetricKeyType; + + if (asymmetricKeyType === 'rsa') { + keyType = 'ssh-rsa'; + } else if (asymmetricKeyType === 'ed25519') { + keyType = 'ssh-ed25519'; + } else if (asymmetricKeyType === 'ec') { + // For EC keys, we need to check the curve + keyType = 'ecdsa-sha2-nistp256'; // Default assumption for P-256 + } + + // Use ssh2 to generate SSH format public key + let finalPublicKey = publicKeyString; // PEM fallback + let formatType = 'pem'; + + try { + const ssh2PrivateKey = ssh2Utils.parseKey(privateKey, keyPassword); + if (!(ssh2PrivateKey instanceof Error)) { + const publicKeyBuffer = ssh2PrivateKey.getPublicSSH(); + const base64Data = publicKeyBuffer.toString('base64'); + finalPublicKey = `${keyType} ${base64Data}`; + formatType = 'ssh'; + console.log("SSH format public key generated!"); + } else { + console.warn("ssh2 parsing failed, using PEM format"); + } + } catch (sshError) { + console.warn("ssh2 failed, using PEM format"); } const response = { success: true, - publicKey: keyInfo.publicKey, - keyType: keyInfo.keyType + publicKey: finalPublicKey, + keyType: keyType, + format: formatType }; - console.log("Sending response:", response); + console.log("Final response publicKey type:", typeof response.publicKey); + console.log("Final response publicKey format:", response.format); + console.log("Final response publicKey length:", response.publicKey.length); + console.log("Public key generated successfully using crypto module:", keyType); + res.json(response); } catch (error) { console.error("Exception in generate-public-key endpoint:", error); diff --git a/src/backend/utils/ssh-key-utils.ts b/src/backend/utils/ssh-key-utils.ts index 55d65568..438b0c27 100644 --- a/src/backend/utils/ssh-key-utils.ts +++ b/src/backend/utils/ssh-key-utils.ts @@ -1,3 +1,7 @@ +// Import SSH2 using ES modules +import ssh2Pkg from 'ssh2'; +const ssh2Utils = ssh2Pkg.utils; + // Simple fallback SSH key type detection function detectKeyTypeFromContent(keyContent: string): string { const content = keyContent.trim(); @@ -67,6 +71,52 @@ function detectKeyTypeFromContent(keyContent: string): string { return 'ecdsa-sha2-nistp256'; // Default ECDSA type } + // Check for PKCS#8 format (modern format) + if (content.includes('-----BEGIN PRIVATE KEY-----')) { + // Try to decode and analyze the DER structure for better detection + try { + const base64Content = content + .replace('-----BEGIN PRIVATE KEY-----', '') + .replace('-----END PRIVATE KEY-----', '') + .replace(/\s/g, ''); + + const decoded = Buffer.from(base64Content, 'base64'); + const decodedString = decoded.toString('binary'); + + // Check for algorithm identifiers in the DER structure + if (decodedString.includes('1.2.840.113549.1.1.1')) { + // RSA OID + return 'ssh-rsa'; + } else if (decodedString.includes('1.2.840.10045.2.1')) { + // EC Private Key OID - this indicates ECDSA + if (decodedString.includes('1.2.840.10045.3.1.7')) { + // prime256v1 curve OID + return 'ecdsa-sha2-nistp256'; + } + return 'ecdsa-sha2-nistp256'; // Default to P-256 + } else if (decodedString.includes('1.3.101.112')) { + // Ed25519 OID + return 'ssh-ed25519'; + } + } catch (error) { + // If decoding fails, fall back to length-based detection + console.warn('Failed to decode private key for type detection:', error); + } + + // Fallback: Try to detect key type from the content structure + // This is a fallback for PKCS#8 format keys + if (content.length < 800) { + // Ed25519 keys are typically shorter + return 'ssh-ed25519'; + } else if (content.length > 1600) { + // RSA keys are typically longer + return 'ssh-rsa'; + } else { + // ECDSA keys are typically medium length + return 'ecdsa-sha2-nistp256'; + } + } + return 'unknown'; } @@ -94,6 +144,52 @@ function detectPublicKeyTypeFromContent(publicKeyContent: string): string { return 'ssh-dss'; } + // Check for PEM format public keys + if (content.includes('-----BEGIN PUBLIC KEY-----')) { + // Try to decode the base64 content to detect key type + try { + const base64Content = content + .replace('-----BEGIN PUBLIC KEY-----', '') + .replace('-----END PUBLIC KEY-----', '') + .replace(/\s/g, ''); + + const decoded = Buffer.from(base64Content, 'base64'); + const decodedString = decoded.toString('binary'); + + // Check for algorithm identifiers in the DER structure + if (decodedString.includes('1.2.840.113549.1.1.1')) { + // RSA OID + return 'ssh-rsa'; + } else if (decodedString.includes('1.2.840.10045.2.1')) { + // EC Public Key OID - this indicates ECDSA + if (decodedString.includes('1.2.840.10045.3.1.7')) { + // prime256v1 curve OID + return 'ecdsa-sha2-nistp256'; + } + return 'ecdsa-sha2-nistp256'; // Default to P-256 + } else if (decodedString.includes('1.3.101.112')) { + // Ed25519 OID + return 'ssh-ed25519'; + } + } catch (error) { + // If decoding fails, fall back to length-based detection + console.warn('Failed to decode public key for type detection:', error); + } + + // Fallback: Try to guess based on key length + if (content.length < 400) { + return 'ssh-ed25519'; + } else if (content.length > 600) { + return 'ssh-rsa'; + } else { + return 'ecdsa-sha2-nistp256'; + } + } + + if (content.includes('-----BEGIN RSA PUBLIC KEY-----')) { + return 'ssh-rsa'; + } + // Check for base64 encoded key data patterns if (content.includes('AAAAB3NzaC1yc2E')) { return 'ssh-rsa'; @@ -117,44 +213,6 @@ function detectPublicKeyTypeFromContent(publicKeyContent: string): string { return 'unknown'; } -// Try multiple import approaches for SSH2 -let ssh2Utils: any = null; - -try { - // Approach 1: Default import - console.log('Trying SSH2 default import...'); - const ssh2Default = require('ssh2'); - console.log('SSH2 default import result:', typeof ssh2Default); - console.log('SSH2 utils from default:', typeof ssh2Default?.utils); - - if (ssh2Default && ssh2Default.utils) { - ssh2Utils = ssh2Default.utils; - console.log('Using SSH2 from default import'); - } -} catch (error) { - console.log('SSH2 default import failed:', error instanceof Error ? error.message : error); -} - -if (!ssh2Utils) { - try { - // Approach 2: Direct utils import - console.log('Trying SSH2 utils direct import...'); - const ssh2UtilsDirect = require('ssh2').utils; - console.log('SSH2 utils direct import result:', typeof ssh2UtilsDirect); - - if (ssh2UtilsDirect) { - ssh2Utils = ssh2UtilsDirect; - console.log('Using SSH2 from direct utils import'); - } - } catch (error) { - console.log('SSH2 utils direct import failed:', error instanceof Error ? error.message : error); - } -} - -if (!ssh2Utils) { - console.error('Failed to import SSH2 utils with any method - using fallback detection'); -} - export interface KeyInfo { privateKey: string; publicKey: string; @@ -211,9 +269,32 @@ export function parseSSHKey(privateKeyData: string, passphrase?: string): KeyInf try { console.log('Attempting to generate public key...'); const publicKeyBuffer = parsedKey.getPublicSSH(); - // Handle SSH public key format properly - publicKey = publicKeyBuffer.toString('utf8').trim(); - console.log('Public key generated, length:', publicKey.length); + console.log('Public key buffer type:', typeof publicKeyBuffer); + console.log('Public key buffer is Buffer:', Buffer.isBuffer(publicKeyBuffer)); + + // ssh2's getPublicSSH() returns binary SSH protocol data, not text + // We need to convert this to proper SSH public key format + if (Buffer.isBuffer(publicKeyBuffer)) { + // Convert binary SSH data to base64 and create proper SSH key format + const base64Data = publicKeyBuffer.toString('base64'); + + // Create proper SSH public key format: "keytype base64data" + if (keyType === 'ssh-rsa') { + publicKey = `ssh-rsa ${base64Data}`; + } else if (keyType === 'ssh-ed25519') { + publicKey = `ssh-ed25519 ${base64Data}`; + } else if (keyType.startsWith('ecdsa-')) { + publicKey = `${keyType} ${base64Data}`; + } else { + publicKey = `${keyType} ${base64Data}`; + } + + console.log('Generated SSH public key format, length:', publicKey.length); + console.log('Public key starts with:', publicKey.substring(0, 50)); + } else { + console.warn('Unexpected public key buffer type'); + publicKey = ''; + } } catch (error) { console.warn('Failed to generate public key:', error); publicKey = ''; @@ -227,6 +308,8 @@ export function parseSSHKey(privateKeyData: string, passphrase?: string): KeyInf } catch (error) { console.warn('SSH2 parsing exception:', error instanceof Error ? error.message : error); } + } else { + console.warn('SSH2 parseKey function not available'); } // Fallback to content-based detection diff --git a/src/locales/en/translation.json b/src/locales/en/translation.json index 04876431..1926015a 100644 --- a/src/locales/en/translation.json +++ b/src/locales/en/translation.json @@ -143,7 +143,14 @@ "publicKeyGeneratedSuccessfully": "Public key generated successfully", "detectedKeyType": "Detected key type", "detectingKeyType": "detecting...", - "optional": "Optional" + "optional": "Optional", + "generateKeyPair": "Generate New Key Pair", + "generateEd25519": "Generate Ed25519", + "generateECDSA": "Generate ECDSA", + "generateRSA": "Generate RSA", + "keyPairGeneratedSuccessfully": "{{keyType}} key pair generated successfully", + "failedToGenerateKeyPair": "Failed to generate key pair", + "generateKeyPairNote": "Generate a new SSH key pair directly. This will replace any existing keys in the form." }, "sshTools": { "title": "SSH Tools", diff --git a/src/locales/zh/translation.json b/src/locales/zh/translation.json index 0d0fa27a..e9b168a6 100644 --- a/src/locales/zh/translation.json +++ b/src/locales/zh/translation.json @@ -142,7 +142,14 @@ "publicKeyGeneratedSuccessfully": "公钥生成成功", "detectedKeyType": "检测到的密钥类型", "detectingKeyType": "检测中...", - "optional": "可选" + "optional": "可选", + "generateKeyPair": "生成新的密钥对", + "generateEd25519": "生成 Ed25519", + "generateECDSA": "生成 ECDSA", + "generateRSA": "生成 RSA", + "keyPairGeneratedSuccessfully": "{{keyType}} 密钥对生成成功", + "failedToGenerateKeyPair": "生成密钥对失败", + "generateKeyPairNote": "直接生成新的SSH密钥对。这将替换表单中的现有密钥。" }, "sshTools": { "title": "SSH 工具", diff --git a/src/ui/Desktop/Apps/Credentials/CredentialEditor.tsx b/src/ui/Desktop/Apps/Credentials/CredentialEditor.tsx index fefa6ec7..40d22471 100644 --- a/src/ui/Desktop/Apps/Credentials/CredentialEditor.tsx +++ b/src/ui/Desktop/Apps/Credentials/CredentialEditor.tsx @@ -25,6 +25,7 @@ import { detectKeyType, detectPublicKeyType, generatePublicKeyFromPrivate, + generateKeyPair, } from "@/ui/main-axios"; import { useTranslation } from "react-i18next"; import type { @@ -299,12 +300,12 @@ export function CredentialEditor({ const getFriendlyKeyTypeName = (keyType: string): string => { const keyTypeMap: Record = { - 'ssh-rsa': 'RSA', - 'ssh-ed25519': 'Ed25519', - 'ecdsa-sha2-nistp256': 'ECDSA P-256', - 'ecdsa-sha2-nistp384': 'ECDSA P-384', - 'ecdsa-sha2-nistp521': 'ECDSA P-521', - 'ssh-dss': 'DSA', + 'ssh-rsa': 'RSA (SSH)', + 'ssh-ed25519': 'Ed25519 (SSH)', + 'ecdsa-sha2-nistp256': 'ECDSA P-256 (SSH)', + 'ecdsa-sha2-nistp384': 'ECDSA P-384 (SSH)', + 'ecdsa-sha2-nistp521': 'ECDSA P-521 (SSH)', + 'ssh-dss': 'DSA (SSH)', 'rsa-sha2-256': 'RSA-SHA2-256', 'rsa-sha2-512': 'RSA-SHA2-512', 'invalid': 'Invalid Key', @@ -669,6 +670,95 @@ export function CredentialEditor({
+ {/* Generate Key Pair Buttons */} +
+ + {t("credentials.generateKeyPair")} + +
+ + + +
+
+ {t("credentials.generateKeyPairNote")} +
+
{ + try { + const response = await authApi.post("/credentials/generate-key-pair", { + keyType, + keySize, + passphrase, + }); + return response.data; + } catch (error) { + throw handleApiError(error, "generate SSH key pair"); + } +} diff --git a/unified_key_section.tsx b/unified_key_section.tsx new file mode 100644 index 00000000..621219a9 --- /dev/null +++ b/unified_key_section.tsx @@ -0,0 +1,266 @@ + +
+ {/* Private Key Section */} +
+ + {t("credentials.sshPrivateKey")} + + +
+ {/* File Upload */} + ( + + + {t("hosts.uploadFile")} + + +
+ { + const file = e.target.files?.[0]; + if (file) { + field.onChange(file); + try { + const fileContent = await file.text(); + debouncedKeyDetection(fileContent, form.watch("keyPassword")); + } catch (error) { + console.error('Failed to read uploaded file:', error); + } + } + }} + className="absolute inset-0 w-full h-full opacity-0 cursor-pointer" + /> + +
+
+
+ )} + /> + + {/* Text Input */} + ( + + + {t("hosts.pasteKey")} + + +