Implement Enterprise-Grade Database Encryption System #244
@@ -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);
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -142,7 +142,14 @@
|
||||
"publicKeyGeneratedSuccessfully": "公钥生成成功",
|
||||
"detectedKeyType": "检测到的密钥类型",
|
||||
"detectingKeyType": "检测中...",
|
||||
"optional": "可选"
|
||||
"optional": "可选",
|
||||
"generateKeyPair": "生成新的密钥对",
|
||||
"generateEd25519": "生成 Ed25519",
|
||||
"generateECDSA": "生成 ECDSA",
|
||||
"generateRSA": "生成 RSA",
|
||||
"keyPairGeneratedSuccessfully": "{{keyType}} 密钥对生成成功",
|
||||
"failedToGenerateKeyPair": "生成密钥对失败",
|
||||
"generateKeyPairNote": "直接生成新的SSH密钥对。这将替换表单中的现有密钥。"
|
||||
},
|
||||
"sshTools": {
|
||||
"title": "SSH 工具",
|
||||
|
||||
@@ -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<string, string> = {
|
||||
'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({
|
||||
</TabsContent>
|
||||
<TabsContent value="key">
|
||||
<div className="mt-4">
|
||||
{/* Generate Key Pair Buttons */}
|
||||
<div className="mb-4 p-4 bg-muted/20 border border-muted rounded-md">
|
||||
<FormLabel className="mb-3 font-bold block">
|
||||
{t("credentials.generateKeyPair")}
|
||||
</FormLabel>
|
||||
<div className="flex gap-2 flex-wrap">
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={async () => {
|
||||
try {
|
||||
const keyPassword = form.watch("keyPassword");
|
||||
const result = await generateKeyPair('ssh-ed25519', undefined, keyPassword);
|
||||
|
||||
if (result.success) {
|
||||
form.setValue("key", result.privateKey);
|
||||
form.setValue("publicKey", result.publicKey);
|
||||
debouncedKeyDetection(result.privateKey, keyPassword);
|
||||
debouncedPublicKeyDetection(result.publicKey);
|
||||
toast.success(t("credentials.keyPairGeneratedSuccessfully", { keyType: "Ed25519" }));
|
||||
} else {
|
||||
toast.error(result.error || t("credentials.failedToGenerateKeyPair"));
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to generate Ed25519 key pair:', error);
|
||||
toast.error(t("credentials.failedToGenerateKeyPair"));
|
||||
}
|
||||
}}
|
||||
>
|
||||
{t("credentials.generateEd25519")}
|
||||
</Button>
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={async () => {
|
||||
try {
|
||||
const keyPassword = form.watch("keyPassword");
|
||||
const result = await generateKeyPair('ecdsa-sha2-nistp256', undefined, keyPassword);
|
||||
|
||||
if (result.success) {
|
||||
form.setValue("key", result.privateKey);
|
||||
form.setValue("publicKey", result.publicKey);
|
||||
debouncedKeyDetection(result.privateKey, keyPassword);
|
||||
debouncedPublicKeyDetection(result.publicKey);
|
||||
toast.success(t("credentials.keyPairGeneratedSuccessfully", { keyType: "ECDSA" }));
|
||||
} else {
|
||||
toast.error(result.error || t("credentials.failedToGenerateKeyPair"));
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to generate ECDSA key pair:', error);
|
||||
toast.error(t("credentials.failedToGenerateKeyPair"));
|
||||
}
|
||||
}}
|
||||
>
|
||||
{t("credentials.generateECDSA")}
|
||||
</Button>
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={async () => {
|
||||
try {
|
||||
const keyPassword = form.watch("keyPassword");
|
||||
const result = await generateKeyPair('ssh-rsa', 2048, keyPassword);
|
||||
|
||||
if (result.success) {
|
||||
form.setValue("key", result.privateKey);
|
||||
form.setValue("publicKey", result.publicKey);
|
||||
debouncedKeyDetection(result.privateKey, keyPassword);
|
||||
debouncedPublicKeyDetection(result.publicKey);
|
||||
toast.success(t("credentials.keyPairGeneratedSuccessfully", { keyType: "RSA" }));
|
||||
} else {
|
||||
toast.error(result.error || t("credentials.failedToGenerateKeyPair"));
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to generate RSA key pair:', error);
|
||||
toast.error(t("credentials.failedToGenerateKeyPair"));
|
||||
}
|
||||
}}
|
||||
>
|
||||
{t("credentials.generateRSA")}
|
||||
</Button>
|
||||
</div>
|
||||
<div className="text-xs text-muted-foreground mt-2">
|
||||
{t("credentials.generateKeyPairNote")}
|
||||
</div>
|
||||
</div>
|
||||
<div className="grid grid-cols-2 gap-4 items-start">
|
||||
<Controller
|
||||
control={form.control}
|
||||
|
||||
@@ -1726,3 +1726,20 @@ export async function generatePublicKeyFromPrivate(
|
||||
throw handleApiError(error, "generate public key from private key");
|
||||
}
|
||||
}
|
||||
|
||||
export async function generateKeyPair(
|
||||
keyType: 'ssh-ed25519' | 'ssh-rsa' | 'ecdsa-sha2-nistp256',
|
||||
keySize?: number,
|
||||
passphrase?: string,
|
||||
): Promise<any> {
|
||||
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");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,266 @@
|
||||
<TabsContent value="key">
|
||||
|
⚠️ Potential issue Insecure file upload pattern - accepts all file types. The file input accepts Apply this diff to restrict file types: 📝 Committable suggestion
🤖 Prompt for AI Agents_⚠️ Potential issue_
**Insecure file upload pattern - accepts all file types.**
The file input accepts `*` along with specific extensions, which allows users to upload any file type. This could lead to uploading malicious files or non-key files.
Apply this diff to restrict file types:
```diff
- accept="*,.pem,.key,.txt,.ppk"
+ accept=".pem,.key,.txt,.ppk"
```
<!-- suggestion_start -->
<details>
<summary>📝 Committable suggestion</summary>
> ‼️ **IMPORTANT**
> Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
```suggestion
accept=".pem,.key,.txt,.ppk"
```
</details>
<!-- suggestion_end -->
<details>
<summary>🤖 Prompt for AI Agents</summary>
```
In unified_key_section.tsx around line 24 the file input's accept attribute
includes a wildcard "*" which allows any file type; update the accept attribute
to remove the "*" and list only the allowed extensions/MIME types (e.g.,
.pem,.key,.txt,.ppk and/or specific key-related MIME types) and ensure any
client-side restriction is complemented by server-side validation that enforces
allowed file types and sizes before processing or storing uploads.
```
</details>
<!-- fingerprinting:phantom:medusa:armadillo -->
<!-- This is an auto-generated comment by CodeRabbit -->
|
||||
<div className="space-y-6">
|
||||
{/* Private Key Section */}
|
||||
<div className="space-y-4">
|
||||
<FormLabel className="text-sm font-medium">
|
||||
{t("credentials.sshPrivateKey")}
|
||||
</FormLabel>
|
||||
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
{/* File Upload */}
|
||||
<Controller
|
||||
control={form.control}
|
||||
name="key"
|
||||
render={({ field }) => (
|
||||
<FormItem className="flex flex-col">
|
||||
<FormLabel className="text-xs text-muted-foreground">
|
||||
{t("hosts.uploadFile")}
|
||||
</FormLabel>
|
||||
<FormControl>
|
||||
<div className="relative inline-block w-full">
|
||||
<input
|
||||
id="key-upload"
|
||||
type="file"
|
||||
accept="*,.pem,.key,.txt,.ppk"
|
||||
onChange={async (e) => {
|
||||
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"
|
||||
/>
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
className="w-full justify-start text-left"
|
||||
>
|
||||
<span className="truncate">
|
||||
{field.value instanceof File
|
||||
? field.value.name
|
||||
: t("credentials.upload")}
|
||||
</span>
|
||||
</Button>
|
||||
</div>
|
||||
</FormControl>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
{/* Text Input */}
|
||||
<Controller
|
||||
control={form.control}
|
||||
name="key"
|
||||
render={({ field }) => (
|
||||
<FormItem className="flex flex-col">
|
||||
<FormLabel className="text-xs text-muted-foreground">
|
||||
{t("hosts.pasteKey")}
|
||||
</FormLabel>
|
||||
<FormControl>
|
||||
<textarea
|
||||
placeholder={t("placeholders.pastePrivateKey")}
|
||||
className="flex min-h-[120px] w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50"
|
||||
value={typeof field.value === "string" ? field.value : ""}
|
||||
onChange={(e) => {
|
||||
field.onChange(e.target.value);
|
||||
debouncedKeyDetection(e.target.value, form.watch("keyPassword"));
|
||||
}}
|
||||
/>
|
||||
</FormControl>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Key type detection display */}
|
||||
{detectedKeyType && (
|
||||
<div className="text-sm">
|
||||
<span className="text-muted-foreground">{t("credentials.detectedKeyType")}: </span>
|
||||
<span className={`font-medium ${
|
||||
detectedKeyType === 'invalid' || detectedKeyType === 'error'
|
||||
? 'text-destructive'
|
||||
: 'text-green-600'
|
||||
}`}>
|
||||
{getFriendlyKeyTypeName(detectedKeyType)}
|
||||
</span>
|
||||
{keyDetectionLoading && (
|
||||
<span className="ml-2 text-muted-foreground">({t("credentials.detecting")}...)</span>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Show existing private key for editing */}
|
||||
{editingCredential && fullCredentialDetails?.key && (
|
||||
<FormItem>
|
||||
<FormLabel>{t("credentials.sshPrivateKey")} ({t("hosts.existingKey")})</FormLabel>
|
||||
<FormControl>
|
||||
<textarea
|
||||
readOnly
|
||||
className="flex min-h-[120px] w-full rounded-md border border-input bg-muted px-3 py-2 text-sm ring-offset-background placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50"
|
||||
value={fullCredentialDetails.key}
|
||||
/>
|
||||
</FormControl>
|
||||
<div className="text-xs text-muted-foreground mt-1">
|
||||
{t("credentials.currentKeyContent")}
|
||||
</div>
|
||||
{fullCredentialDetails?.detectedKeyType && (
|
||||
<div className="text-sm mt-2">
|
||||
<span className="text-muted-foreground">Key type: </span>
|
||||
<span className="font-medium text-green-600">
|
||||
{getFriendlyKeyTypeName(fullCredentialDetails.detectedKeyType)}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
</FormItem>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Public Key Section */}
|
||||
<div className="space-y-4">
|
||||
<FormLabel className="text-sm font-medium">
|
||||
{t("credentials.sshPublicKey")} ({t("credentials.optional")})
|
||||
</FormLabel>
|
||||
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
{/* File Upload */}
|
||||
<Controller
|
||||
control={form.control}
|
||||
name="publicKey"
|
||||
render={({ field }) => (
|
||||
<FormItem className="flex flex-col">
|
||||
<FormLabel className="text-xs text-muted-foreground">
|
||||
{t("hosts.uploadFile")}
|
||||
</FormLabel>
|
||||
<FormControl>
|
||||
<div className="relative inline-block w-full">
|
||||
<input
|
||||
id="public-key-upload"
|
||||
type="file"
|
||||
accept="*,.pub,.txt"
|
||||
onChange={async (e) => {
|
||||
const file = e.target.files?.[0];
|
||||
if (file) {
|
||||
try {
|
||||
const fileContent = await file.text();
|
||||
field.onChange(fileContent);
|
||||
debouncedPublicKeyDetection(fileContent);
|
||||
} catch (error) {
|
||||
console.error('Failed to read uploaded public key file:', error);
|
||||
}
|
||||
}
|
||||
}}
|
||||
className="absolute inset-0 w-full h-full opacity-0 cursor-pointer"
|
||||
/>
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
className="w-full justify-start text-left"
|
||||
>
|
||||
<span className="truncate">
|
||||
{field.value ? t("credentials.publicKeyUploaded") : t("credentials.uploadPublicKey")}
|
||||
</span>
|
||||
</Button>
|
||||
</div>
|
||||
</FormControl>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
{/* Text Input */}
|
||||
<Controller
|
||||
control={form.control}
|
||||
name="publicKey"
|
||||
render={({ field }) => (
|
||||
<FormItem className="flex flex-col">
|
||||
<FormLabel className="text-xs text-muted-foreground">
|
||||
{t("hosts.pasteKey")}
|
||||
</FormLabel>
|
||||
<FormControl>
|
||||
<textarea
|
||||
placeholder={t("placeholders.pastePublicKey")}
|
||||
className="flex min-h-[80px] w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50"
|
||||
value={field.value || ""}
|
||||
onChange={(e) => {
|
||||
field.onChange(e.target.value);
|
||||
debouncedPublicKeyDetection(e.target.value);
|
||||
}}
|
||||
/>
|
||||
</FormControl>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Public key type detection */}
|
||||
{detectedPublicKeyType && form.watch("publicKey") && (
|
||||
<div className="text-sm">
|
||||
<span className="text-muted-foreground">{t("credentials.detectedKeyType")}: </span>
|
||||
<span className={`font-medium ${
|
||||
detectedPublicKeyType === 'invalid' || detectedPublicKeyType === 'error'
|
||||
? 'text-destructive'
|
||||
: 'text-green-600'
|
||||
}`}>
|
||||
{getFriendlyKeyTypeName(detectedPublicKeyType)}
|
||||
</span>
|
||||
{publicKeyDetectionLoading && (
|
||||
<span className="ml-2 text-muted-foreground">({t("credentials.detecting")}...)</span>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="text-xs text-muted-foreground">
|
||||
{t("credentials.publicKeyNote")}
|
||||
</div>
|
||||
|
||||
{/* Show existing public key for editing */}
|
||||
{editingCredential && fullCredentialDetails?.publicKey && (
|
||||
<FormItem>
|
||||
<FormLabel>{t("credentials.sshPublicKey")} ({t("hosts.existingKey")})</FormLabel>
|
||||
<FormControl>
|
||||
<textarea
|
||||
readOnly
|
||||
className="flex min-h-[80px] w-full rounded-md border border-input bg-muted px-3 py-2 text-sm ring-offset-background placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50"
|
||||
value={fullCredentialDetails.publicKey}
|
||||
/>
|
||||
</FormControl>
|
||||
<div className="text-xs text-muted-foreground mt-1">
|
||||
{t("credentials.currentPublicKeyContent")}
|
||||
</div>
|
||||
</FormItem>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Generate Public Key Button */}
|
||||
{form.watch("key") && (
|
||||
<div className="mt-4">
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={handleGeneratePublicKey}
|
||||
disabled={generatePublicKeyLoading}
|
||||
className="w-full"
|
||||
>
|
||||
{generatePublicKeyLoading ? (
|
||||
<>
|
||||
<span className="mr-2">{t("credentials.generating")}...</span>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<span>{t("credentials.generatePublicKey")}</span>
|
||||
</>
|
||||
)}
|
||||
</Button>
|
||||
<p className="text-xs text-muted-foreground mt-2 text-center">
|
||||
{t("credentials.generatePublicKeyNote")}
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</TabsContent>
|
||||
Reference in New Issue
Block a user
⚠️ Potential issue
Critical: Binary string conversion loses data integrity
Converting Buffer to 'binary' string can corrupt the data due to character encoding issues. This could lead to incorrect key type detection.
Then search for the key type indicators directly in the Buffer:
📝 Committable suggestion
🤖 Prompt for AI Agents
⚠️ Potential issue
Critical: Binary string conversion issue repeated
Same binary string conversion issue that can corrupt data during PKCS#8 format detection.
Search for OIDs directly in the Buffer:
📝 Committable suggestion
🤖 Prompt for AI Agents
⚠️ Potential issue
Unreliable key type detection based on content length
Using content length to determine key type is highly unreliable and could lead to incorrect key type identification. Key sizes can vary significantly within the same key type.
Consider removing this fallback or implementing a more robust detection mechanism:
📝 Committable suggestion
🤖 Prompt for AI Agents
⚠️ Potential issue
Critical: Binary string conversion in public key detection
Same issue with binary string conversion that can corrupt data during public key type detection.
🤖 Prompt for AI Agents
⚠️ Potential issue
Unreliable public key type detection based on length
Using content length for public key type detection is unreliable and error-prone.
📝 Committable suggestion
⚠️ Potential issue
Missing passphrase parameter in detectKeyType function
The
detectKeyTypefunction doesn't accept a passphrase parameter, which means it will fail for encrypted keys. This is inconsistent with theparseSSHKeyfunction.📝 Committable suggestion
🤖 Prompt for AI Agents
⚠️ Potential issue
Misleading validation result when unable to verify
Returning
isValid: truewhen unable to verify the key pair is misleading and could lead to security issues.📝 Committable suggestion
🤖 Prompt for AI Agents