Implement direct SSH key generation with ssh2 native API
- Replace complex PEM-to-SSH conversion logic with ssh2's generateKeyPairSync - Add three key generation buttons: Ed25519, ECDSA P-256, and RSA - Generate keys directly in SSH format (ssh-ed25519, ecdsa-sha2-nistp256, ssh-rsa) - Fix ECDSA parameter bug: use bits (256) instead of curve for ssh2 API - Enhance generate-public-key endpoint with SSH format conversion - Add comprehensive key type detection and parsing fallbacks - Add internationalization support for key generation UI - Simplify codebase from 300+ lines to ~80 lines of clean SSH generation 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user