Implement Enterprise-Grade Database Encryption System #244

Merged
ZacharyZcR merged 10 commits from main into dev-1.7.0 2025-09-16 03:00:00 +00:00
7 changed files with 764 additions and 62 deletions
Showing only changes of commit 9cf0a14cea - Show all commits
+244 -12
View File
@@ -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();
3
@@ -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);
+124 -41
View File
@@ -1,3 +1,7 @@
// Import SSH2 using ES modules
coderabbitai[bot] commented 2025-09-16 02:49:57 +00:00 (Migrated from github.com)
Review

⚠️ 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.

-      const decoded = Buffer.from(base64Content, 'base64').toString('binary');
+      const decoded = Buffer.from(base64Content, 'base64');

Then search for the key type indicators directly in the Buffer:

-      if (decoded.includes('ssh-rsa')) {
+      if (decoded.includes(Buffer.from('ssh-rsa'))) {
📝 Committable suggestion

‼️ 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.

      const decoded = Buffer.from(base64Content, 'base64');
      if (decoded.includes(Buffer.from('ssh-rsa'))) {
🤖 Prompt for AI Agents
In src/backend/utils/ssh-key-utils.ts around lines 36-37, the code converts the
decoded Buffer to a 'binary' string which can corrupt data; instead keep the
decoded value as a Buffer (const decoded = Buffer.from(base64Content, 'base64'))
and detect key type by searching the Buffer directly (use
Buffer.prototype.includes or indexOf with Buffer.from('ssh-') /
Buffer.from('BEGIN') / other key markers) rather than calling
toString('binary'); update downstream checks to operate on the Buffer or
explicitly decode to 'utf8' only when safe.
_⚠️ 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. ```diff - const decoded = Buffer.from(base64Content, 'base64').toString('binary'); + const decoded = Buffer.from(base64Content, 'base64'); ``` Then search for the key type indicators directly in the Buffer: ```diff - if (decoded.includes('ssh-rsa')) { + if (decoded.includes(Buffer.from('ssh-rsa'))) { ``` <!-- 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 const decoded = Buffer.from(base64Content, 'base64'); if (decoded.includes(Buffer.from('ssh-rsa'))) { ``` </details> <!-- suggestion_end --> <details> <summary>🤖 Prompt for AI Agents</summary> ``` In src/backend/utils/ssh-key-utils.ts around lines 36-37, the code converts the decoded Buffer to a 'binary' string which can corrupt data; instead keep the decoded value as a Buffer (const decoded = Buffer.from(base64Content, 'base64')) and detect key type by searching the Buffer directly (use Buffer.prototype.includes or indexOf with Buffer.from('ssh-') / Buffer.from('BEGIN') / other key markers) rather than calling toString('binary'); update downstream checks to operate on the Buffer or explicitly decode to 'utf8' only when safe. ``` </details> <!-- fingerprinting:phantom:medusa:armadillo --> <!-- This is an auto-generated comment by CodeRabbit -->
coderabbitai[bot] commented 2025-09-16 02:49:57 +00:00 (Migrated from github.com)
Review

⚠️ Potential issue

Critical: Binary string conversion issue repeated

Same binary string conversion issue that can corrupt data during PKCS#8 format detection.

       const decoded = Buffer.from(base64Content, 'base64');
-      const decodedString = decoded.toString('binary');
+      // Work directly with the Buffer instead

Search for OIDs directly in the Buffer:

-      if (decodedString.includes('1.2.840.113549.1.1.1')) {
+      // Convert OID string to Buffer for comparison
+      const rsaOid = Buffer.from([0x2A, 0x86, 0x48, 0x86, 0xF7, 0x0D, 0x01, 0x01, 0x01]);
+      if (decoded.includes(rsaOid)) {
📝 Committable suggestion

‼️ 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.

      const decoded = Buffer.from(base64Content, 'base64');
      // Work directly with the Buffer instead

      // Convert OID string to Buffer for comparison
      const rsaOid = Buffer.from([0x2A, 0x86, 0x48, 0x86, 0xF7, 0x0D, 0x01, 0x01, 0x01]);
      if (decoded.includes(rsaOid)) {
        // existing handling for RSA OID...
      }
🤖 Prompt for AI Agents
In src/backend/utils/ssh-key-utils.ts around lines 83-84 the code decodes base64
into a string using decoded.toString('binary'), which can corrupt binary data
and break PKCS#8 OID detection; instead keep the data as a Buffer and search for
OID byte sequences directly (e.g., use Buffer.indexOf or Buffer.includes with
Buffer.from([ ...OID bytes... ]) to detect the PKCS#8 OID), removing any
toString('binary') conversion and basing the detection on the raw Buffer.
_⚠️ Potential issue_ **Critical: Binary string conversion issue repeated** Same binary string conversion issue that can corrupt data during PKCS#8 format detection. ```diff const decoded = Buffer.from(base64Content, 'base64'); - const decodedString = decoded.toString('binary'); + // Work directly with the Buffer instead ``` Search for OIDs directly in the Buffer: ```diff - if (decodedString.includes('1.2.840.113549.1.1.1')) { + // Convert OID string to Buffer for comparison + const rsaOid = Buffer.from([0x2A, 0x86, 0x48, 0x86, 0xF7, 0x0D, 0x01, 0x01, 0x01]); + if (decoded.includes(rsaOid)) { ``` <!-- 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 const decoded = Buffer.from(base64Content, 'base64'); // Work directly with the Buffer instead // Convert OID string to Buffer for comparison const rsaOid = Buffer.from([0x2A, 0x86, 0x48, 0x86, 0xF7, 0x0D, 0x01, 0x01, 0x01]); if (decoded.includes(rsaOid)) { // existing handling for RSA OID... } ``` </details> <!-- suggestion_end --> <details> <summary>🤖 Prompt for AI Agents</summary> ``` In src/backend/utils/ssh-key-utils.ts around lines 83-84 the code decodes base64 into a string using decoded.toString('binary'), which can corrupt binary data and break PKCS#8 OID detection; instead keep the data as a Buffer and search for OID byte sequences directly (e.g., use Buffer.indexOf or Buffer.includes with Buffer.from([ ...OID bytes... ]) to detect the PKCS#8 OID), removing any toString('binary') conversion and basing the detection on the raw Buffer. ``` </details> <!-- fingerprinting:phantom:medusa:armadillo --> <!-- This is an auto-generated comment by CodeRabbit -->
coderabbitai[bot] commented 2025-09-16 02:49:57 +00:00 (Migrated from github.com)
Review

⚠️ 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:

-    // 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';
-    }
+    // Unable to determine key type from PKCS#8 content
+    return 'unknown';
📝 Committable suggestion

‼️ 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.

    // Unable to determine key type from PKCS#8 content
    return 'unknown';
🤖 Prompt for AI Agents
In src/backend/utils/ssh-key-utils.ts around lines 108 to 117, the fallback that
infers key type from content.length is unreliable; remove the length-based
heuristics and instead detect the key type from the SSH public key text (split
the content by whitespace and use the first token as the key type when present),
and if that fails, avoid guessing — return null or throw an explicit error (or
implement a proper parser that decodes the base64 blob and inspects key
metadata) so callers can handle unknown types rather than relying on
length-based defaults.
_⚠️ 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: ```diff - // 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'; - } + // Unable to determine key type from PKCS#8 content + return 'unknown'; ``` <!-- 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 // Unable to determine key type from PKCS#8 content return 'unknown'; ``` </details> <!-- suggestion_end --> <details> <summary>🤖 Prompt for AI Agents</summary> ``` In src/backend/utils/ssh-key-utils.ts around lines 108 to 117, the fallback that infers key type from content.length is unreliable; remove the length-based heuristics and instead detect the key type from the SSH public key text (split the content by whitespace and use the first token as the key type when present), and if that fails, avoid guessing — return null or throw an explicit error (or implement a proper parser that decodes the base64 blob and inspects key metadata) so callers can handle unknown types rather than relying on length-based defaults. ``` </details> <!-- fingerprinting:phantom:medusa:armadillo --> <!-- This is an auto-generated comment by CodeRabbit -->
coderabbitai[bot] commented 2025-09-16 02:49:58 +00:00 (Migrated from github.com)
Review

⚠️ 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.

       const decoded = Buffer.from(base64Content, 'base64');
-      const decodedString = decoded.toString('binary');
+      // Work directly with the Buffer

Committable suggestion skipped: line range outside the PR's diff.

🤖 Prompt for AI Agents
In src/backend/utils/ssh-key-utils.ts around lines 157-158, the code uses
decoded.toString('binary') which can corrupt data when detecting public key
types; replace the binary encoding with a UTF-8-safe approach (e.g.,
decoded.toString('utf8') or simply decoded.toString()) or avoid converting to a
string entirely and operate on the Buffer (check prefixes like Buffer.compare or
slice to match "ssh-" bytes) so public key detection remains lossless and
accurate.
_⚠️ 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. ```diff const decoded = Buffer.from(base64Content, 'base64'); - const decodedString = decoded.toString('binary'); + // Work directly with the Buffer ``` > Committable suggestion skipped: line range outside the PR's diff. <details> <summary>🤖 Prompt for AI Agents</summary> ``` In src/backend/utils/ssh-key-utils.ts around lines 157-158, the code uses decoded.toString('binary') which can corrupt data when detecting public key types; replace the binary encoding with a UTF-8-safe approach (e.g., decoded.toString('utf8') or simply decoded.toString()) or avoid converting to a string entirely and operate on the Buffer (check prefixes like Buffer.compare or slice to match "ssh-" bytes) so public key detection remains lossless and accurate. ``` </details> <!-- fingerprinting:phantom:medusa:armadillo --> <!-- This is an auto-generated comment by CodeRabbit -->
coderabbitai[bot] commented 2025-09-16 02:49:58 +00:00 (Migrated from github.com)
Review

⚠️ Potential issue

Unreliable public key type detection based on length

Using content length for public key type detection is unreliable and error-prone.

-    // 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';
-    }
+    // Unable to determine public key type from PEM content
+    return 'unknown';
📝 Committable suggestion

‼️ 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.

    // Unable to determine public key type from PEM content
    return 'unknown';
_⚠️ Potential issue_ **Unreliable public key type detection based on length** Using content length for public key type detection is unreliable and error-prone. ```diff - // 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'; - } + // Unable to determine public key type from PEM content + return 'unknown'; ``` <!-- 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 // Unable to determine public key type from PEM content return 'unknown'; ``` </details> <!-- suggestion_end --> <!-- fingerprinting:phantom:medusa:armadillo --> <!-- This is an auto-generated comment by CodeRabbit -->
coderabbitai[bot] commented 2025-09-16 02:49:58 +00:00 (Migrated from github.com)
Review

⚠️ Potential issue

Missing passphrase parameter in detectKeyType function

The detectKeyType function doesn't accept a passphrase parameter, which means it will fail for encrypted keys. This is inconsistent with the parseSSHKey function.

-export function detectKeyType(privateKeyData: string): string {
+export function detectKeyType(privateKeyData: string, passphrase?: string): string {
   try {
-    const parsedKey = ssh2Utils.parseKey(privateKeyData);
+    const parsedKey = ssh2Utils.parseKey(privateKeyData, passphrase);
📝 Committable suggestion

‼️ 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.

export function detectKeyType(privateKeyData: string, passphrase?: string): string {
  try {
    const parsedKey = ssh2Utils.parseKey(privateKeyData, passphrase);
    if (parsedKey instanceof Error) {
      return 'unknown';
    }
    return parsedKey.type || 'unknown';
  } catch (error) {
    return 'unknown';
  }
}
🤖 Prompt for AI Agents
In src/backend/utils/ssh-key-utils.ts around lines 396 to 406, detectKeyType
currently doesn't accept a passphrase so it fails on encrypted keys; change its
signature to accept an optional passphrase parameter (string | Buffer |
undefined) and forward that passphrase into ssh2Utils.parseKey (use the same
call shape as parseSSHKey in this file), preserving the existing Error-instance
and catch handling and returning parsedKey.type || 'unknown' or 'unknown' on
errors. Ensure the function signature and any callers are updated to pass a
passphrase when available.
_⚠️ Potential issue_ **Missing passphrase parameter in detectKeyType function** The `detectKeyType` function doesn't accept a passphrase parameter, which means it will fail for encrypted keys. This is inconsistent with the `parseSSHKey` function. ```diff -export function detectKeyType(privateKeyData: string): string { +export function detectKeyType(privateKeyData: string, passphrase?: string): string { try { - const parsedKey = ssh2Utils.parseKey(privateKeyData); + const parsedKey = ssh2Utils.parseKey(privateKeyData, passphrase); ``` <!-- 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 export function detectKeyType(privateKeyData: string, passphrase?: string): string { try { const parsedKey = ssh2Utils.parseKey(privateKeyData, passphrase); if (parsedKey instanceof Error) { return 'unknown'; } return parsedKey.type || 'unknown'; } catch (error) { return 'unknown'; } } ``` </details> <!-- suggestion_end --> <details> <summary>🤖 Prompt for AI Agents</summary> ``` In src/backend/utils/ssh-key-utils.ts around lines 396 to 406, detectKeyType currently doesn't accept a passphrase so it fails on encrypted keys; change its signature to accept an optional passphrase parameter (string | Buffer | undefined) and forward that passphrase into ssh2Utils.parseKey (use the same call shape as parseSSHKey in this file), preserving the existing Error-instance and catch handling and returning parsedKey.type || 'unknown' or 'unknown' on errors. Ensure the function signature and any callers are updated to pass a passphrase when available. ``` </details> <!-- fingerprinting:phantom:medusa:armadillo --> <!-- This is an auto-generated comment by CodeRabbit -->
coderabbitai[bot] commented 2025-09-16 02:49:58 +00:00 (Migrated from github.com)
Review

⚠️ Potential issue

Misleading validation result when unable to verify

Returning isValid: true when unable to verify the key pair is misleading and could lead to security issues.

   // If we can't generate public key or compare, just check if types match
   return {
-    isValid: true, // Assume valid if types match and no errors
+    isValid: false, // Cannot verify without comparison
     privateKeyType: privateKeyInfo.keyType,
     publicKeyType: publicKeyInfo.keyType,
-    error: 'Unable to verify key pair match, but key types are compatible'
+    error: 'Unable to verify key pair - manual verification required'
   };
📝 Committable suggestion

‼️ 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.

      isValid: false, // Cannot verify without comparison
      privateKeyType: privateKeyInfo.keyType,
      publicKeyType: publicKeyInfo.keyType,
      error: 'Unable to verify key pair - manual verification required'
    };
🤖 Prompt for AI Agents
In src/backend/utils/ssh-key-utils.ts around lines 512 to 516, the function
currently returns isValid: true when key types match but actual pair
verification failed, which is misleading; change the result to isValid: false
(or add a separate verified:false field) when you cannot confirm the key pair
match, include the underlying error message or reason in the error property, and
ensure any callers treat this case as a validation failure rather than a
success.
_⚠️ Potential issue_ **Misleading validation result when unable to verify** Returning `isValid: true` when unable to verify the key pair is misleading and could lead to security issues. ```diff // If we can't generate public key or compare, just check if types match return { - isValid: true, // Assume valid if types match and no errors + isValid: false, // Cannot verify without comparison privateKeyType: privateKeyInfo.keyType, publicKeyType: publicKeyInfo.keyType, - error: 'Unable to verify key pair match, but key types are compatible' + error: 'Unable to verify key pair - manual verification required' }; ``` <!-- 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 isValid: false, // Cannot verify without comparison privateKeyType: privateKeyInfo.keyType, publicKeyType: publicKeyInfo.keyType, error: 'Unable to verify key pair - manual verification required' }; ``` </details> <!-- suggestion_end --> <details> <summary>🤖 Prompt for AI Agents</summary> ``` In src/backend/utils/ssh-key-utils.ts around lines 512 to 516, the function currently returns isValid: true when key types match but actual pair verification failed, which is misleading; change the result to isValid: false (or add a separate verified:false field) when you cannot confirm the key pair match, include the underlying error message or reason in the error property, and ensure any callers treat this case as a validation failure rather than a success. ``` </details> <!-- fingerprinting:phantom:medusa:armadillo --> <!-- This is an auto-generated comment by CodeRabbit -->
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
+8 -1
View File
@@ -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",
+8 -1
View File
@@ -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}
+17
View File
@@ -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");
}
}
+266
View File
@@ -0,0 +1,266 @@
<TabsContent value="key">
coderabbitai[bot] commented 2025-09-16 02:49:58 +00:00 (Migrated from github.com)
Review

⚠️ 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:

-                                      accept="*,.pem,.key,.txt,.ppk"
+                                      accept=".pem,.key,.txt,.ppk"
📝 Committable suggestion

‼️ 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.

                                      accept=".pem,.key,.txt,.ppk"
🤖 Prompt for AI Agents
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.
_⚠️ 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>