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 1113 additions and 78 deletions
Showing only changes of commit 301303079b - Show all commits
+4 -1
View File
@@ -137,9 +137,12 @@ export const sshCredentials = sqliteTable("ssh_credentials", {
authType: text("auth_type").notNull(),
username: text("username").notNull(),
password: text("password"),
key: text("key", { length: 16384 }),
key: text("key", { length: 16384 }), // backward compatibility
privateKey: text("private_key", { length: 16384 }),
publicKey: text("public_key", { length: 4096 }),
keyPassword: text("key_password"),
keyType: text("key_type"),
detectedKeyType: text("detected_key_type"),
usageCount: integer("usage_count").notNull().default(0),
lastUsed: text("last_used"),
createdAt: text("created_at")
+170 -3
View File
@@ -5,6 +5,7 @@ import { eq, and, desc, sql } from "drizzle-orm";
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";
const router = express.Router();
@@ -109,6 +110,22 @@ router.post("/", authenticateJWT, async (req: Request, res: Response) => {
const plainKeyPassword =
authType === "key" && keyPassword ? keyPassword : null;
let keyInfo = null;
if (authType === "key" && plainKey) {
keyInfo = parseSSHKey(plainKey, plainKeyPassword);
if (!keyInfo.success) {
authLogger.warn("SSH key parsing failed", {
operation: "credential_create",
userId,
name,
error: keyInfo.error,
});
return res.status(400).json({
error: `Invalid SSH key: ${keyInfo.error}`
});
}
}
const credentialData = {
userId,
name: name.trim(),
@@ -118,9 +135,12 @@ router.post("/", authenticateJWT, async (req: Request, res: Response) => {
authType,
username: username.trim(),
password: plainPassword,
key: plainKey,
key: plainKey, // backward compatibility
privateKey: keyInfo?.privateKey || plainKey,
publicKey: keyInfo?.publicKey || null,
keyPassword: plainKeyPassword,
keyType: keyType || null,
detectedKeyType: keyInfo?.keyType || null,
usageCount: 0,
lastUsed: null,
};
@@ -248,7 +268,13 @@ router.get("/:id", authenticateJWT, async (req: Request, res: Response) => {
(output as any).password = credential.password;
}
if (credential.key) {
(output as any).key = credential.key;
(output as any).key = credential.key; // backward compatibility
}
if (credential.privateKey) {
(output as any).privateKey = credential.privateKey;
}
if (credential.publicKey) {
(output as any).publicKey = credential.publicKey;
}
if (credential.keyPassword) {
(output as any).keyPassword = credential.keyPassword;
@@ -314,7 +340,26 @@ router.put("/:id", authenticateJWT, async (req: Request, res: Response) => {
updateFields.password = updateData.password || null;
}
if (updateData.key !== undefined) {
updateFields.key = updateData.key || null;
updateFields.key = updateData.key || null; // backward compatibility
// Parse SSH key if provided
if (updateData.key && existing[0].authType === "key") {
const keyInfo = parseSSHKey(updateData.key, updateData.keyPassword);
if (!keyInfo.success) {
authLogger.warn("SSH key parsing failed during update", {
operation: "credential_update",
userId,
credentialId: parseInt(id),
error: keyInfo.error,
});
return res.status(400).json({
error: `Invalid SSH key: ${keyInfo.error}`
});
}
updateFields.privateKey = keyInfo.privateKey;
updateFields.publicKey = keyInfo.publicKey;
updateFields.detectedKeyType = keyInfo.keyType;
}
}
if (updateData.keyPassword !== undefined) {
updateFields.keyPassword = updateData.keyPassword || null;
@@ -585,6 +630,7 @@ function formatCredentialOutput(credential: any): any {
authType: credential.authType,
username: credential.username,
keyType: credential.keyType,
detectedKeyType: credential.detectedKeyType,
usageCount: credential.usageCount || 0,
lastUsed: credential.lastUsed,
createdAt: credential.createdAt,
@@ -661,4 +707,125 @@ router.put(
},
);
// Detect SSH key type endpoint
// POST /credentials/detect-key-type
router.post("/detect-key-type", authenticateJWT, async (req: Request, res: Response) => {
const { privateKey, keyPassword } = req.body;
console.log("=== Key Detection API Called ===");
console.log("Request body keys:", Object.keys(req.body));
console.log("Private key provided:", !!privateKey);
console.log("Private key type:", typeof privateKey);
if (!privateKey || typeof privateKey !== "string") {
console.log("Invalid private key provided");
return res.status(400).json({ error: "Private key is required" });
}
try {
console.log("Calling parseSSHKey...");
const keyInfo = parseSSHKey(privateKey, keyPassword);
console.log("parseSSHKey result:", keyInfo);
const response = {
success: keyInfo.success,
keyType: keyInfo.keyType,
detectedKeyType: keyInfo.keyType,
hasPublicKey: !!keyInfo.publicKey,
error: keyInfo.error || null
};
console.log("Sending response:", response);
res.json(response);
} catch (error) {
console.error("Exception in detect-key-type endpoint:", error);
authLogger.error("Failed to detect key type", error);
res.status(500).json({
error: error instanceof Error ? error.message : "Failed to detect key type"
});
}
});
// Detect SSH public key type endpoint
// POST /credentials/detect-public-key-type
router.post("/detect-public-key-type", authenticateJWT, async (req: Request, res: Response) => {
const { publicKey } = req.body;
console.log("=== Public Key Detection API Called ===");
console.log("Request body keys:", Object.keys(req.body));
coderabbitai[bot] commented 2025-09-16 02:49:56 +00:00 (Migrated from github.com)
Review

⚠️ Potential issue

Excessive console logging in production endpoints.

The key detection endpoints contain extensive console.log statements that could expose sensitive information in production logs.

Remove or guard console.log statements:

-  console.log("=== Key Detection API Called ===");
-  console.log("Request body keys:", Object.keys(req.body));
-  console.log("Private key provided:", !!privateKey);
-  console.log("Private key type:", typeof privateKey);
+  if (process.env.NODE_ENV === 'development') {
+    console.log("Key detection API called with keys:", Object.keys(req.body));
+  }
📝 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.

  if (process.env.NODE_ENV === 'development') {
    console.log("Key detection API called with keys:", Object.keys(req.body));
  }

  if (!privateKey || typeof privateKey !== "string") {
    console.log("Invalid private key provided");
    return res.status(400).json({ error: "Private key is required" });
  }

  try {
    console.log("Calling parseSSHKey...");
    const keyInfo = parseSSHKey(privateKey, keyPassword);
    console.log("parseSSHKey result:", keyInfo);

    const response = {
      success: keyInfo.success,
      keyType: keyInfo.keyType,
      detectedKeyType: keyInfo.keyType,
      hasPublicKey: !!keyInfo.publicKey,
      error: keyInfo.error || null
    };

    console.log("Sending response:", response);
    res.json(response);
  } catch (error) {
    console.error("Exception in detect-key-type endpoint:", error);
    authLogger.error("Failed to detect key type", error);
    res.status(500).json({
      error: error instanceof Error ? error.message : "Failed to detect key type"
    });
  }
});
🤖 Prompt for AI Agents
In src/backend/database/routes/credentials.ts around lines 763 to 795, there are
multiple console.log statements that can leak sensitive key material; remove
these console.log calls and replace them with minimal, non-sensitive logging via
the existing logger (e.g. authLogger.debug/info) behind an environment check
(NODE_ENV !== "production" or a dedicated DEBUG flag), never log raw privateKey
or key material (log boolean flags like hasPrivateKey or masked/sanitized
indicators only), and ensure errors use authLogger.error with the error object
but without including secret contents before sending generic error responses to
the client.
_⚠️ Potential issue_ **Excessive console logging in production endpoints.** The key detection endpoints contain extensive console.log statements that could expose sensitive information in production logs. Remove or guard console.log statements: ```diff - console.log("=== Key Detection API Called ==="); - console.log("Request body keys:", Object.keys(req.body)); - console.log("Private key provided:", !!privateKey); - console.log("Private key type:", typeof privateKey); + if (process.env.NODE_ENV === 'development') { + console.log("Key detection API called with keys:", Object.keys(req.body)); + } ``` <!-- 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 if (process.env.NODE_ENV === 'development') { console.log("Key detection API called with keys:", Object.keys(req.body)); } if (!privateKey || typeof privateKey !== "string") { console.log("Invalid private key provided"); return res.status(400).json({ error: "Private key is required" }); } try { console.log("Calling parseSSHKey..."); const keyInfo = parseSSHKey(privateKey, keyPassword); console.log("parseSSHKey result:", keyInfo); const response = { success: keyInfo.success, keyType: keyInfo.keyType, detectedKeyType: keyInfo.keyType, hasPublicKey: !!keyInfo.publicKey, error: keyInfo.error || null }; console.log("Sending response:", response); res.json(response); } catch (error) { console.error("Exception in detect-key-type endpoint:", error); authLogger.error("Failed to detect key type", error); res.status(500).json({ error: error instanceof Error ? error.message : "Failed to detect key type" }); } }); ``` </details> <!-- suggestion_end --> <details> <summary>🤖 Prompt for AI Agents</summary> ``` In src/backend/database/routes/credentials.ts around lines 763 to 795, there are multiple console.log statements that can leak sensitive key material; remove these console.log calls and replace them with minimal, non-sensitive logging via the existing logger (e.g. authLogger.debug/info) behind an environment check (NODE_ENV !== "production" or a dedicated DEBUG flag), never log raw privateKey or key material (log boolean flags like hasPrivateKey or masked/sanitized indicators only), and ensure errors use authLogger.error with the error object but without including secret contents before sending generic error responses to the client. ``` </details> <!-- fingerprinting:phantom:medusa:armadillo --> <!-- This is an auto-generated comment by CodeRabbit -->
coderabbitai[bot] commented 2025-09-16 02:49:56 +00:00 (Migrated from github.com)
Review

⚠️ Potential issue

Unsafe grep pattern in key existence check.

Using the key content directly in grep could fail or cause issues with special regex characters.

-          const keyPattern = publicKey.split(' ')[1]; // Get the key part without algorithm
-          conn.exec(`grep -q "${keyPattern}" ~/.ssh/authorized_keys 2>/dev/null`, (err, stream) => {
+          // Use grep -F for literal string matching
+          const keyPattern = publicKey.split(' ')[1];
+          const escapedPattern = keyPattern.replace(/[[\]{}()*+?.\\^$|]/g, '\\$&');
+          conn.exec(`grep -qF "${escapedPattern}" ~/.ssh/authorized_keys 2>/dev/null`, (err, stream) => {
📝 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.

// Use grep -F with proper shell quoting of the pattern
const keyPattern = publicKey.split(' ')[1]; // Get the key part without algorithm
const safePattern = keyPattern.replace(/'/g, "'\\''"); // escape single quotes for safe single-quoting
conn.exec(`grep -qF -- '${safePattern}' ~/.ssh/authorized_keys 2>/dev/null`, (err, stream) => {
🤖 Prompt for AI Agents
In src/backend/database/routes/credentials.ts around lines 1157-1158, the code
injects the raw key content into a shell grep command which is unsafe for
regex/shell metacharacters and can cause failures or injections; replace this
with a literal search (e.g., use grep -F -q with the key as a single argument)
or, even better, avoid shell interpolation entirely by reading
~/.ssh/authorized_keys in Node and checking for the key with a literal string
comparison (or use child_process.execFile/spawn with the key passed as an
argument) so the key is treated as data, not a regex or shell code.
_⚠️ Potential issue_ **Unsafe grep pattern in key existence check.** Using the key content directly in grep could fail or cause issues with special regex characters. ```diff - const keyPattern = publicKey.split(' ')[1]; // Get the key part without algorithm - conn.exec(`grep -q "${keyPattern}" ~/.ssh/authorized_keys 2>/dev/null`, (err, stream) => { + // Use grep -F for literal string matching + const keyPattern = publicKey.split(' ')[1]; + const escapedPattern = keyPattern.replace(/[[\]{}()*+?.\\^$|]/g, '\\$&'); + conn.exec(`grep -qF "${escapedPattern}" ~/.ssh/authorized_keys 2>/dev/null`, (err, stream) => { ``` <!-- 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 // Use grep -F with proper shell quoting of the pattern const keyPattern = publicKey.split(' ')[1]; // Get the key part without algorithm const safePattern = keyPattern.replace(/'/g, "'\\''"); // escape single quotes for safe single-quoting conn.exec(`grep -qF -- '${safePattern}' ~/.ssh/authorized_keys 2>/dev/null`, (err, stream) => { ``` </details> <!-- suggestion_end --> <details> <summary>🤖 Prompt for AI Agents</summary> ``` In src/backend/database/routes/credentials.ts around lines 1157-1158, the code injects the raw key content into a shell grep command which is unsafe for regex/shell metacharacters and can cause failures or injections; replace this with a literal search (e.g., use grep -F -q with the key as a single argument) or, even better, avoid shell interpolation entirely by reading ~/.ssh/authorized_keys in Node and checking for the key with a literal string comparison (or use child_process.execFile/spawn with the key passed as an argument) so the key is treated as data, not a regex or shell code. ``` </details> <!-- fingerprinting:phantom:medusa:armadillo --> <!-- This is an auto-generated comment by CodeRabbit -->
coderabbitai[bot] commented 2025-09-16 02:49:56 +00:00 (Migrated from github.com)
Review

⚠️ Potential issue

Command injection vulnerability in SSH key deployment.

The public key is directly interpolated into shell commands without proper escaping, which could allow command injection.

The current escaping only handles single quotes but not other shell metacharacters:

-          const escapedKey = publicKey.replace(/'/g, "'\\''");
-          conn.exec(`echo '${escapedKey}' >> ~/.ssh/authorized_keys && chmod 600 ~/.ssh/authorized_keys`, (err, stream) => {
+          // Use SFTP to write the key safely without shell interpretation
+          conn.sftp((err, sftp) => {
+            if (err) return rejectAdd(err);
+            const stream = sftp.createWriteStream('~/.ssh/authorized_keys', { flags: 'a', mode: 0o600 });
+            stream.write(publicKey + '\n');
+            stream.end(() => resolveAdd());
+          });

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

🤖 Prompt for AI Agents
In src/backend/database/routes/credentials.ts around lines 1175-1176, the code
interpolates the publicKey into a shell command leading to possible command
injection; instead, validate and sanitize the key (ensure it matches an accepted
SSH public key pattern, strip newlines/whitespace), then avoid embedding it in a
shell command by using the SSH2 SFTP API (conn.sftp()) to safely create/ensure
~/.ssh exists with 0700, open or create ~/.ssh/authorized_keys, append the
validated key with a trailing newline, and set authorized_keys to 0600; if SFTP
isn’t available, use a safe remote API that writes raw bytes (not shell
interpolation) or execute a single quoted ssh-escaped printf via a parameterized
exec helper that does not allow shell expansion.
_⚠️ Potential issue_ **Command injection vulnerability in SSH key deployment.** The public key is directly interpolated into shell commands without proper escaping, which could allow command injection. The current escaping only handles single quotes but not other shell metacharacters: ```diff - const escapedKey = publicKey.replace(/'/g, "'\\''"); - conn.exec(`echo '${escapedKey}' >> ~/.ssh/authorized_keys && chmod 600 ~/.ssh/authorized_keys`, (err, stream) => { + // Use SFTP to write the key safely without shell interpretation + conn.sftp((err, sftp) => { + if (err) return rejectAdd(err); + const stream = sftp.createWriteStream('~/.ssh/authorized_keys', { flags: 'a', mode: 0o600 }); + stream.write(publicKey + '\n'); + stream.end(() => resolveAdd()); + }); ``` > Committable suggestion skipped: line range outside the PR's diff. <details> <summary>🤖 Prompt for AI Agents</summary> ``` In src/backend/database/routes/credentials.ts around lines 1175-1176, the code interpolates the publicKey into a shell command leading to possible command injection; instead, validate and sanitize the key (ensure it matches an accepted SSH public key pattern, strip newlines/whitespace), then avoid embedding it in a shell command by using the SSH2 SFTP API (conn.sftp()) to safely create/ensure ~/.ssh exists with 0700, open or create ~/.ssh/authorized_keys, append the validated key with a trailing newline, and set authorized_keys to 0600; if SFTP isn’t available, use a safe remote API that writes raw bytes (not shell interpolation) or execute a single quoted ssh-escaped printf via a parameterized exec helper that does not allow shell expansion. ``` </details> <!-- fingerprinting:phantom:medusa:armadillo --> <!-- This is an auto-generated comment by CodeRabbit -->
console.log("Public key provided:", !!publicKey);
console.log("Public key type:", typeof publicKey);
if (!publicKey || typeof publicKey !== "string") {
console.log("Invalid public key provided");
return res.status(400).json({ error: "Public key is required" });
}
try {
console.log("Calling parsePublicKey...");
const keyInfo = parsePublicKey(publicKey);
console.log("parsePublicKey result:", keyInfo);
const response = {
success: keyInfo.success,
keyType: keyInfo.keyType,
detectedKeyType: keyInfo.keyType,
error: keyInfo.error || null
};
console.log("Sending response:", response);
res.json(response);
} catch (error) {
console.error("Exception in detect-public-key-type endpoint:", error);
authLogger.error("Failed to detect public key type", error);
res.status(500).json({
error: error instanceof Error ? error.message : "Failed to detect public key type"
});
}
});
// Validate SSH key pair endpoint
// POST /credentials/validate-key-pair
router.post("/validate-key-pair", authenticateJWT, async (req: Request, res: Response) => {
const { privateKey, publicKey, keyPassword } = req.body;
console.log("=== Key Pair Validation API Called ===");
console.log("Request body keys:", Object.keys(req.body));
console.log("Private key provided:", !!privateKey);
console.log("Public key provided:", !!publicKey);
if (!privateKey || typeof privateKey !== "string") {
console.log("Invalid private key provided");
return res.status(400).json({ error: "Private key is required" });
}
if (!publicKey || typeof publicKey !== "string") {
console.log("Invalid public key provided");
return res.status(400).json({ error: "Public key is required" });
}
try {
console.log("Calling validateKeyPair...");
const validationResult = validateKeyPair(privateKey, publicKey, keyPassword);
console.log("validateKeyPair result:", validationResult);
const response = {
isValid: validationResult.isValid,
privateKeyType: validationResult.privateKeyType,
publicKeyType: validationResult.publicKeyType,
generatedPublicKey: validationResult.generatedPublicKey,
error: validationResult.error || null
};
console.log("Sending response:", response);
res.json(response);
} catch (error) {
console.error("Exception in validate-key-pair endpoint:", error);
authLogger.error("Failed to validate key pair", error);
res.status(500).json({
error: error instanceof Error ? error.message : "Failed to validate key pair"
});
}
});
export default router;
+1 -1
View File
@@ -191,7 +191,7 @@ wss.on("connection", (ws: WebSocket) => {
const credential = credentials[0];
resolvedCredentials = {
password: credential.password,
key: credential.key,
key: credential.privateKey || credential.key, // prefer new privateKey field
keyPassword: credential.keyPassword,
keyType: credential.keyType,
authType: credential.authType,
+2 -2
View File
@@ -455,7 +455,7 @@ async function connectSSHTunnel(
const credential = credentials[0];
resolvedSourceCredentials = {
password: credential.password,
sshKey: credential.key,
sshKey: credential.privateKey || credential.key, // prefer new privateKey field
keyPassword: credential.keyPassword,
keyType: credential.keyType,
authMethod: credential.authType,
@@ -501,7 +501,7 @@ async function connectSSHTunnel(
const credential = credentials[0];
resolvedEndpointCredentials = {
password: credential.password,
sshKey: credential.key,
sshKey: credential.privateKey || credential.key, // prefer new privateKey field
keyPassword: credential.keyPassword,
keyType: credential.keyType,
authMethod: credential.authType,
+444
View File
@@ -0,0 +1,444 @@
// Simple fallback SSH key type detection
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 -->
function detectKeyTypeFromContent(keyContent: string): string {
const content = keyContent.trim();
// Check for OpenSSH format headers
if (content.includes('-----BEGIN OPENSSH PRIVATE KEY-----')) {
// Look for key type indicators in the content
if (content.includes('ssh-ed25519') || content.includes('AAAAC3NzaC1lZDI1NTE5')) {
return 'ssh-ed25519';
}
if (content.includes('ssh-rsa') || content.includes('AAAAB3NzaC1yc2E')) {
return 'ssh-rsa';
}
if (content.includes('ecdsa-sha2-nistp256')) {
return 'ecdsa-sha2-nistp256';
}
if (content.includes('ecdsa-sha2-nistp384')) {
return 'ecdsa-sha2-nistp384';
}
if (content.includes('ecdsa-sha2-nistp521')) {
return 'ecdsa-sha2-nistp521';
}
// For OpenSSH format, try to detect by analyzing the base64 content structure
try {
const base64Content = content
.replace('-----BEGIN OPENSSH PRIVATE KEY-----', '')
.replace('-----END OPENSSH PRIVATE KEY-----', '')
.replace(/\s/g, '');
// OpenSSH format starts with "openssh-key-v1" followed by key type
const decoded = Buffer.from(base64Content, 'base64').toString('binary');
if (decoded.includes('ssh-rsa')) {
return 'ssh-rsa';
}
if (decoded.includes('ssh-ed25519')) {
return 'ssh-ed25519';
}
if (decoded.includes('ecdsa-sha2-nistp256')) {
return 'ecdsa-sha2-nistp256';
}
if (decoded.includes('ecdsa-sha2-nistp384')) {
return 'ecdsa-sha2-nistp384';
}
if (decoded.includes('ecdsa-sha2-nistp521')) {
return 'ecdsa-sha2-nistp521';
}
// Default to RSA for OpenSSH format if we can't detect specifically
return 'ssh-rsa';
} catch (error) {
console.warn('Failed to decode OpenSSH key content:', error);
// If decoding fails, default to RSA as it's most common for OpenSSH format
return 'ssh-rsa';
}
}
// Check for traditional PEM headers
if (content.includes('-----BEGIN RSA PRIVATE KEY-----')) {
return 'ssh-rsa';
}
if (content.includes('-----BEGIN DSA PRIVATE KEY-----')) {
return 'ssh-dss';
}
if (content.includes('-----BEGIN EC PRIVATE KEY-----')) {
return 'ecdsa-sha2-nistp256'; // Default ECDSA type
}
return 'unknown';
}
// Detect public key type from public key content
function detectPublicKeyTypeFromContent(publicKeyContent: string): string {
const content = publicKeyContent.trim();
// SSH public keys start with the key type
if (content.startsWith('ssh-rsa ')) {
return 'ssh-rsa';
}
if (content.startsWith('ssh-ed25519 ')) {
return 'ssh-ed25519';
}
if (content.startsWith('ecdsa-sha2-nistp256 ')) {
return 'ecdsa-sha2-nistp256';
}
if (content.startsWith('ecdsa-sha2-nistp384 ')) {
return 'ecdsa-sha2-nistp384';
}
if (content.startsWith('ecdsa-sha2-nistp521 ')) {
return 'ecdsa-sha2-nistp521';
}
if (content.startsWith('ssh-dss ')) {
return 'ssh-dss';
}
// Check for base64 encoded key data patterns
if (content.includes('AAAAB3NzaC1yc2E')) {
return 'ssh-rsa';
}
if (content.includes('AAAAC3NzaC1lZDI1NTE5')) {
return 'ssh-ed25519';
}
if (content.includes('AAAAE2VjZHNhLXNoYTItbmlzdHAyNTY')) {
return 'ecdsa-sha2-nistp256';
}
if (content.includes('AAAAE2VjZHNhLXNoYTItbmlzdHAzODQ')) {
return 'ecdsa-sha2-nistp384';
}
if (content.includes('AAAAE2VjZHNhLXNoYTItbmlzdHA1MjE')) {
return 'ecdsa-sha2-nistp521';
}
if (content.includes('AAAAB3NzaC1kc3M')) {
return 'ssh-dss';
}
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;
keyType: string;
success: boolean;
error?: string;
}
export interface PublicKeyInfo {
publicKey: string;
keyType: string;
success: boolean;
error?: string;
}
export interface KeyPairValidationResult {
isValid: boolean;
privateKeyType: string;
publicKeyType: string;
generatedPublicKey?: string;
error?: string;
}
/**
* Parse SSH private key and extract public key and type information
*/
export function parseSSHKey(privateKeyData: string, passphrase?: string): KeyInfo {
console.log('=== SSH Key Parsing Debug ===');
console.log('Key length:', privateKeyData?.length || 'undefined');
console.log('First 100 chars:', privateKeyData?.substring(0, 100) || 'undefined');
console.log('ssh2Utils available:', typeof ssh2Utils);
console.log('parseKey function available:', typeof ssh2Utils?.parseKey);
try {
let keyType = 'unknown';
let publicKey = '';
let useSSH2 = false;
// Try SSH2 first if available
if (ssh2Utils && typeof ssh2Utils.parseKey === 'function') {
try {
console.log('Calling ssh2Utils.parseKey...');
const parsedKey = ssh2Utils.parseKey(privateKeyData, passphrase);
console.log('parseKey returned:', typeof parsedKey, parsedKey instanceof Error ? parsedKey.message : 'success');
if (!(parsedKey instanceof Error)) {
// Extract key type
if (parsedKey.type) {
keyType = parsedKey.type;
}
console.log('Extracted key type:', keyType);
// Generate public key in SSH format
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);
} catch (error) {
console.warn('Failed to generate public key:', error);
publicKey = '';
}
useSSH2 = true;
console.log(`SSH key parsed successfully with SSH2: ${keyType}`);
} else {
console.warn('SSH2 parsing failed:', parsedKey.message);
}
} catch (error) {
console.warn('SSH2 parsing exception:', error instanceof Error ? error.message : error);
}
}
// Fallback to content-based detection
if (!useSSH2) {
console.log('Using fallback key type detection...');
keyType = detectKeyTypeFromContent(privateKeyData);
console.log(`Fallback detected key type: ${keyType}`);
// For fallback, we can't generate public key but the detection is still useful
publicKey = '';
if (keyType !== 'unknown') {
console.log(`SSH key type detected successfully with fallback: ${keyType}`);
}
}
return {
privateKey: privateKeyData,
publicKey,
keyType,
success: keyType !== 'unknown'
};
} catch (error) {
console.error('Exception during SSH key parsing:', error);
console.error('Error stack:', error instanceof Error ? error.stack : 'No stack');
// Final fallback - try content detection
try {
const fallbackKeyType = detectKeyTypeFromContent(privateKeyData);
if (fallbackKeyType !== 'unknown') {
console.log(`Final fallback detection successful: ${fallbackKeyType}`);
return {
privateKey: privateKeyData,
publicKey: '',
keyType: fallbackKeyType,
success: true
};
}
} catch (fallbackError) {
console.error('Even fallback detection failed:', fallbackError);
}
return {
privateKey: privateKeyData,
publicKey: '',
keyType: 'unknown',
success: false,
error: error instanceof Error ? error.message : 'Unknown error parsing key'
};
}
}
/**
* Parse SSH public key and extract type information
*/
export function parsePublicKey(publicKeyData: string): PublicKeyInfo {
console.log('=== SSH Public Key Parsing Debug ===');
console.log('Public key length:', publicKeyData?.length || 'undefined');
console.log('First 100 chars:', publicKeyData?.substring(0, 100) || 'undefined');
try {
const keyType = detectPublicKeyTypeFromContent(publicKeyData);
console.log(`Public key type detected: ${keyType}`);
return {
publicKey: publicKeyData,
keyType,
success: keyType !== 'unknown'
};
} catch (error) {
console.error('Exception during SSH public key parsing:', error);
return {
publicKey: publicKeyData,
keyType: 'unknown',
success: false,
error: error instanceof Error ? error.message : 'Unknown error parsing public key'
};
}
}
/**
* Detect SSH key type from private key content
*/
export function detectKeyType(privateKeyData: string): string {
try {
const parsedKey = ssh2Utils.parseKey(privateKeyData);
if (parsedKey instanceof Error) {
return 'unknown';
}
return parsedKey.type || 'unknown';
} catch (error) {
return 'unknown';
}
}
/**
* Get friendly key type name
*/
export function 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',
'rsa-sha2-256': 'RSA-SHA2-256',
'rsa-sha2-512': 'RSA-SHA2-512',
'unknown': 'Unknown'
};
return keyTypeMap[keyType] || keyType;
}
/**
* Validate if a private key and public key form a valid key pair
*/
export function validateKeyPair(privateKeyData: string, publicKeyData: string, passphrase?: string): KeyPairValidationResult {
console.log('=== Key Pair Validation Debug ===');
console.log('Private key length:', privateKeyData?.length || 'undefined');
console.log('Public key length:', publicKeyData?.length || 'undefined');
try {
// First parse the private key and try to generate public key
const privateKeyInfo = parseSSHKey(privateKeyData, passphrase);
const publicKeyInfo = parsePublicKey(publicKeyData);
console.log('Private key parsing result:', privateKeyInfo.success, privateKeyInfo.keyType);
console.log('Public key parsing result:', publicKeyInfo.success, publicKeyInfo.keyType);
if (!privateKeyInfo.success) {
return {
isValid: false,
privateKeyType: privateKeyInfo.keyType,
publicKeyType: publicKeyInfo.keyType,
error: `Invalid private key: ${privateKeyInfo.error}`
};
}
if (!publicKeyInfo.success) {
return {
isValid: false,
privateKeyType: privateKeyInfo.keyType,
publicKeyType: publicKeyInfo.keyType,
error: `Invalid public key: ${publicKeyInfo.error}`
};
}
// Check if key types match
if (privateKeyInfo.keyType !== publicKeyInfo.keyType) {
return {
isValid: false,
privateKeyType: privateKeyInfo.keyType,
publicKeyType: publicKeyInfo.keyType,
error: `Key type mismatch: private key is ${privateKeyInfo.keyType}, public key is ${publicKeyInfo.keyType}`
};
}
// If we have a generated public key from the private key, compare them
if (privateKeyInfo.publicKey && privateKeyInfo.publicKey.trim()) {
const generatedPublicKey = privateKeyInfo.publicKey.trim();
const providedPublicKey = publicKeyData.trim();
console.log('Generated public key length:', generatedPublicKey.length);
console.log('Provided public key length:', providedPublicKey.length);
// Compare the key data part (excluding comments)
const generatedKeyParts = generatedPublicKey.split(' ');
const providedKeyParts = providedPublicKey.split(' ');
if (generatedKeyParts.length >= 2 && providedKeyParts.length >= 2) {
// Compare key type and key data (first two parts)
const generatedKeyData = generatedKeyParts[0] + ' ' + generatedKeyParts[1];
const providedKeyData = providedKeyParts[0] + ' ' + providedKeyParts[1];
console.log('Generated key data:', generatedKeyData.substring(0, 50) + '...');
console.log('Provided key data:', providedKeyData.substring(0, 50) + '...');
if (generatedKeyData === providedKeyData) {
return {
isValid: true,
privateKeyType: privateKeyInfo.keyType,
publicKeyType: publicKeyInfo.keyType,
generatedPublicKey: generatedPublicKey
};
} else {
return {
isValid: false,
privateKeyType: privateKeyInfo.keyType,
publicKeyType: publicKeyInfo.keyType,
generatedPublicKey: generatedPublicKey,
error: 'Public key does not match the private key'
};
}
}
}
// 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
privateKeyType: privateKeyInfo.keyType,
publicKeyType: publicKeyInfo.keyType,
error: 'Unable to verify key pair match, but key types are compatible'
};
} catch (error) {
console.error('Exception during key pair validation:', error);
return {
isValid: false,
privateKeyType: 'unknown',
publicKeyType: 'unknown',
error: error instanceof Error ? error.message : 'Unknown error during validation'
};
}
}
@@ -22,6 +22,9 @@ import {
updateCredential,
getCredentials,
getCredentialDetails,
detectKeyType,
detectPublicKeyType,
validateKeyPair,
} from "@/ui/main-axios";
import { useTranslation } from "react-i18next";
import type {
@@ -45,6 +48,20 @@ export function CredentialEditor({
const [keyInputMethod, setKeyInputMethod] = useState<"upload" | "paste">(
"upload",
);
const [detectedKeyType, setDetectedKeyType] = useState<string | null>(null);
const [keyDetectionLoading, setKeyDetectionLoading] = useState(false);
const keyDetectionTimeoutRef = useRef<NodeJS.Timeout | null>(null);
const [detectedPublicKeyType, setDetectedPublicKeyType] = useState<string | null>(null);
const [publicKeyDetectionLoading, setPublicKeyDetectionLoading] = useState(false);
const publicKeyDetectionTimeoutRef = useRef<NodeJS.Timeout | null>(null);
const [keyPairValidation, setKeyPairValidation] = useState<{
isValid: boolean | null;
loading: boolean;
error?: string;
}>({ isValid: null, loading: false });
const keyPairValidationTimeoutRef = useRef<NodeJS.Timeout | null>(null);
useEffect(() => {
const fetchData = async () => {
@@ -101,6 +118,7 @@ export function CredentialEditor({
username: z.string().min(1),
password: z.string().optional(),
key: z.any().optional().nullable(),
publicKey: z.string().optional(),
keyPassword: z.string().optional(),
keyType: z
.enum([
@@ -149,6 +167,7 @@ export function CredentialEditor({
username: "",
password: "",
key: null,
publicKey: "",
keyPassword: "",
keyType: "auto",
},
@@ -169,6 +188,7 @@ export function CredentialEditor({
username: fullCredentialDetails.username || "",
password: "",
key: null,
publicKey: "",
keyPassword: "",
keyType: "auto" as const,
};
@@ -177,6 +197,7 @@ export function CredentialEditor({
formData.password = fullCredentialDetails.password || "";
} else if (defaultAuthType === "key") {
formData.key = fullCredentialDetails.key || "";
formData.publicKey = fullCredentialDetails.publicKey || "";
formData.keyPassword = fullCredentialDetails.keyPassword || "";
formData.keyType =
(fullCredentialDetails.keyType as any) || ("auto" as const);
@@ -196,6 +217,7 @@ export function CredentialEditor({
username: "",
password: "",
key: null,
publicKey: "",
keyPassword: "",
keyType: "auto",
});
@@ -203,6 +225,140 @@ export function CredentialEditor({
}
}, [editingCredential?.id, fullCredentialDetails, form]);
// Cleanup timeout on unmount
useEffect(() => {
return () => {
if (keyDetectionTimeoutRef.current) {
clearTimeout(keyDetectionTimeoutRef.current);
}
if (publicKeyDetectionTimeoutRef.current) {
clearTimeout(publicKeyDetectionTimeoutRef.current);
}
if (keyPairValidationTimeoutRef.current) {
clearTimeout(keyPairValidationTimeoutRef.current);
}
};
}, []);
// Detect key type function
const handleKeyTypeDetection = async (keyValue: string, keyPassword?: string) => {
if (!keyValue || keyValue.trim() === '') {
setDetectedKeyType(null);
return;
}
setKeyDetectionLoading(true);
try {
const result = await detectKeyType(keyValue, keyPassword);
if (result.success) {
setDetectedKeyType(result.keyType);
} else {
setDetectedKeyType('invalid');
console.warn('Key detection failed:', result.error);
}
} catch (error) {
setDetectedKeyType('error');
console.error('Key type detection error:', error);
} finally {
setKeyDetectionLoading(false);
}
};
// Debounced key type detection
const debouncedKeyDetection = (keyValue: string, keyPassword?: string) => {
if (keyDetectionTimeoutRef.current) {
clearTimeout(keyDetectionTimeoutRef.current);
}
keyDetectionTimeoutRef.current = setTimeout(() => {
handleKeyTypeDetection(keyValue, keyPassword);
}, 1000);
};
// Detect public key type function
const handlePublicKeyTypeDetection = async (publicKeyValue: string) => {
if (!publicKeyValue || publicKeyValue.trim() === '') {
setDetectedPublicKeyType(null);
return;
}
setPublicKeyDetectionLoading(true);
try {
const result = await detectPublicKeyType(publicKeyValue);
if (result.success) {
setDetectedPublicKeyType(result.keyType);
} else {
setDetectedPublicKeyType('invalid');
console.warn('Public key detection failed:', result.error);
}
} catch (error) {
setDetectedPublicKeyType('error');
console.error('Public key type detection error:', error);
} finally {
setPublicKeyDetectionLoading(false);
}
};
// Debounced public key type detection
const debouncedPublicKeyDetection = (publicKeyValue: string) => {
if (publicKeyDetectionTimeoutRef.current) {
clearTimeout(publicKeyDetectionTimeoutRef.current);
}
publicKeyDetectionTimeoutRef.current = setTimeout(() => {
handlePublicKeyTypeDetection(publicKeyValue);
}, 1000);
};
// Validate key pair function
const handleKeyPairValidation = async (privateKeyValue: string, publicKeyValue: string, keyPassword?: string) => {
if (!privateKeyValue || privateKeyValue.trim() === '' || !publicKeyValue || publicKeyValue.trim() === '') {
setKeyPairValidation({ isValid: null, loading: false });
return;
}
setKeyPairValidation({ isValid: null, loading: true });
try {
const result = await validateKeyPair(privateKeyValue, publicKeyValue, keyPassword);
setKeyPairValidation({
isValid: result.isValid,
loading: false,
error: result.error
});
} catch (error) {
setKeyPairValidation({
isValid: false,
loading: false,
error: error instanceof Error ? error.message : 'Unknown error during validation'
});
}
};
// Debounced key pair validation
const debouncedKeyPairValidation = (privateKeyValue: string, publicKeyValue: string, keyPassword?: string) => {
if (keyPairValidationTimeoutRef.current) {
clearTimeout(keyPairValidationTimeoutRef.current);
}
keyPairValidationTimeoutRef.current = setTimeout(() => {
handleKeyPairValidation(privateKeyValue, publicKeyValue, keyPassword);
}, 1500); // Slightly longer delay since this is more expensive
};
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',
'rsa-sha2-256': 'RSA-SHA2-256',
'rsa-sha2-512': 'RSA-SHA2-512',
'invalid': 'Invalid Key',
'error': 'Detection Error',
'unknown': 'Unknown'
};
return keyTypeMap[keyType] || keyType;
};
const onSubmit = async (data: FormData) => {
try {
if (!data.name || data.name.trim() === "") {
@@ -221,6 +377,7 @@ export function CredentialEditor({
submitData.password = null;
submitData.key = null;
submitData.publicKey = null;
submitData.keyPassword = null;
submitData.keyType = null;
@@ -233,6 +390,7 @@ export function CredentialEditor({
} else {
submitData.key = data.key;
}
submitData.publicKey = data.publicKey;
submitData.keyPassword = data.keyPassword;
submitData.keyType = data.keyType;
}
@@ -600,17 +758,22 @@ export function CredentialEditor({
if (!editingCredential) {
if (value === "upload") {
form.setValue("key", null);
} else {
form.setValue("publicKey", "");
} else if (value === "paste") {
form.setValue("key", "");
form.setValue("publicKey", "");
}
} else {
// For existing credentials, preserve the key data when switching methods
const currentKey = fullCredentialDetails?.key || "";
const currentPublicKey = fullCredentialDetails?.publicKey || "";
if (value === "paste") {
form.setValue("key", currentKey);
form.setValue("publicKey", currentPublicKey);
} else {
// For upload mode, keep the current string value to show "existing key" status
form.setValue("key", currentKey);
form.setValue("publicKey", currentPublicKey);
}
}
}}
@@ -625,52 +788,159 @@ export function CredentialEditor({
</TabsTrigger>
</TabsList>
<TabsContent value="upload" className="mt-4">
<Controller
control={form.control}
name="key"
render={({ field }) => (
<FormItem className="mb-4">
<FormLabel>
{t("credentials.sshPrivateKey")}
</FormLabel>
<FormControl>
<div className="relative inline-block">
<input
id="key-upload"
type="file"
accept=".pem,.key,.txt,.ppk"
onChange={(e) => {
const file = e.target.files?.[0];
field.onChange(file || null);
}}
className="absolute inset-0 w-full h-full opacity-0 cursor-pointer"
/>
<Button
type="button"
variant="outline"
className="justify-start text-left"
>
<span
className="truncate"
title={
field.value?.name ||
t("credentials.upload")
}
<div className="grid grid-cols-2 gap-4 items-start">
<Controller
control={form.control}
name="key"
render={({ field }) => (
<FormItem className="mb-4 flex flex-col">
<FormLabel className="mb-2 min-h-[20px]">
{t("credentials.sshPrivateKey")}
</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];
field.onChange(file || null);
// Detect key type from uploaded file
if (file) {
try {
const fileContent = await file.text();
debouncedKeyDetection(fileContent, form.watch("keyPassword"));
// Trigger key pair validation if public key is available
const publicKeyValue = form.watch("publicKey");
if (publicKeyValue && publicKeyValue.trim()) {
debouncedKeyPairValidation(fileContent, publicKeyValue, form.watch("keyPassword"));
}
} catch (error) {
console.error('Failed to read uploaded file:', error);
}
} else {
setDetectedKeyType(null);
}
}}
className="absolute inset-0 w-full h-full opacity-0 cursor-pointer"
/>
<Button
type="button"
variant="outline"
className="w-full justify-start text-left"
>
{field.value instanceof File
? field.value.name
: typeof field.value === "string" && field.value && editingCredential
? t("hosts.existingKey") + " - " + t("credentials.updateKey")
: field.value
? t("credentials.updateKey")
: t("credentials.upload")}
<span
className="truncate"
title={
field.value?.name ||
t("credentials.upload")
}
>
{field.value instanceof File
? field.value.name
: typeof field.value === "string" && field.value && editingCredential
? t("hosts.existingKey") + " - " + t("credentials.updateKey")
: field.value
? t("credentials.updateKey")
: t("credentials.upload")}
</span>
</Button>
</div>
</FormControl>
{/* Key type detection display for uploaded files */}
{detectedKeyType && field.value instanceof File && (
<div className="text-sm mt-2">
<span className="text-muted-foreground">Detected key type: </span>
<span className={`font-medium ${
detectedKeyType === 'invalid' || detectedKeyType === 'error'
? 'text-destructive'
: 'text-green-600'
}`}>
{getFriendlyKeyTypeName(detectedKeyType)}
</span>
</Button>
{keyDetectionLoading && (
<span className="ml-2 text-muted-foreground">(detecting...)</span>
)}
</div>
)}
</FormItem>
)}
/>
<Controller
control={form.control}
name="publicKey"
render={({ field }) => (
<FormItem className="mb-4 flex flex-col">
<FormLabel className="mb-2 min-h-[20px]">
{t("credentials.sshPublicKey")} ({t("credentials.optional")})
</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);
// Detect public key type from uploaded file
debouncedPublicKeyDetection(fileContent);
// Trigger key pair validation if private key is available
const privateKeyValue = form.watch("key");
if (privateKeyValue && typeof privateKeyValue === "string" && privateKeyValue.trim()) {
debouncedKeyPairValidation(privateKeyValue, fileContent, form.watch("keyPassword"));
}
} catch (error) {
console.error('Failed to read uploaded public key file:', error);
}
} else {
field.onChange("");
setDetectedPublicKeyType(null);
}
}}
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>
<div className="text-xs text-muted-foreground mt-1">
{t("credentials.publicKeyNote")}
</div>
</FormControl>
</FormItem>
)}
/>
{/* Public key type detection display for upload mode */}
{detectedPublicKeyType && field.value && (
<div className="text-sm mt-2">
<span className="text-muted-foreground">Detected key type: </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">(detecting...)</span>
)}
</div>
)}
</FormItem>
)}
/>
</div>
{/* Show existing key content preview for upload mode */}
{editingCredential && fullCredentialDetails?.key && typeof form.watch("key") === "string" && (
<FormItem className="mb-4">
@@ -685,6 +955,15 @@ export function CredentialEditor({
<div className="text-xs text-muted-foreground mt-1">
Current SSH key content - {t("credentials.uploadFile")} to replace
</div>
{/* Show detected key type for existing credential */}
{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 className="grid grid-cols-15 gap-4 mt-4">
@@ -760,33 +1039,130 @@ export function CredentialEditor({
</div>
</TabsContent>
<TabsContent value="paste" className="mt-4">
<Controller
control={form.control}
name="key"
render={({ field }) => (
<FormItem className="mb-4">
<FormLabel>
{t("credentials.sshPrivateKey")}
</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)
}
/>
</FormControl>
</FormItem>
)}
/>
<div className="grid grid-cols-2 gap-4 items-start">
<Controller
control={form.control}
name="key"
render={({ field }) => (
<FormItem className="mb-4 flex flex-col">
<FormLabel className="mb-2 min-h-[20px]">
{t("credentials.sshPrivateKey")}
</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);
// Trigger key type detection with debounce
debouncedKeyDetection(e.target.value, form.watch("keyPassword"));
// Trigger key pair validation if public key is available
const publicKeyValue = form.watch("publicKey");
if (publicKeyValue && publicKeyValue.trim()) {
debouncedKeyPairValidation(e.target.value, publicKeyValue, form.watch("keyPassword"));
}
}}
/>
</FormControl>
{/* Key type detection display */}
{detectedKeyType && (
<div className="text-sm mt-2">
<span className="text-muted-foreground">Detected key type: </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">(detecting...)</span>
)}
</div>
)}
</FormItem>
)}
/>
<Controller
control={form.control}
name="publicKey"
render={({ field }) => (
<FormItem className="mb-4 flex flex-col">
<FormLabel className="mb-2 min-h-[20px]">
{t("credentials.sshPublicKey")} ({t("credentials.optional")})
</FormLabel>
<FormControl>
<textarea
placeholder={t(
"placeholders.pastePublicKey",
)}
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={field.value || ""}
onChange={(e) => {
field.onChange(e.target.value);
// Trigger public key type detection with debounce
debouncedPublicKeyDetection(e.target.value);
// Trigger key pair validation if private key is available
const privateKeyValue = form.watch("key");
if (privateKeyValue && typeof privateKeyValue === "string" && privateKeyValue.trim()) {
debouncedKeyPairValidation(privateKeyValue, e.target.value, form.watch("keyPassword"));
}
}}
/>
</FormControl>
<div className="text-xs text-muted-foreground mt-1">
{t("credentials.publicKeyNote")}
</div>
{/* Public key type detection display for paste mode */}
{detectedPublicKeyType && field.value && (
<div className="text-sm mt-2">
<span className="text-muted-foreground">Detected key type: </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">(detecting...)</span>
)}
</div>
)}
</FormItem>
)}
/>
</div>
{/* Key pair validation display */}
{(form.watch("key") && form.watch("publicKey") &&
typeof form.watch("key") === "string" && form.watch("key").trim() &&
form.watch("publicKey").trim()) && (
<div className="mt-4 p-3 border rounded-md bg-muted/50">
<div className="text-sm font-medium mb-1">Key Pair Validation</div>
{keyPairValidation.loading && (
<div className="text-sm text-muted-foreground">
<span>Validating key pair...</span>
</div>
)}
{!keyPairValidation.loading && keyPairValidation.isValid === true && (
<div className="text-sm text-green-600 font-medium">
Private and public keys match
</div>
)}
{!keyPairValidation.loading && keyPairValidation.isValid === false && (
<div className="text-sm text-destructive">
Keys do not match: {keyPairValidation.error}
</div>
)}
</div>
)}
<div className="grid grid-cols-15 gap-4 mt-4">
<FormField
control={form.control}
+45
View File
@@ -1666,3 +1666,48 @@ export async function renameCredentialFolder(
throw handleApiError(error, "rename credential folder");
}
}
export async function detectKeyType(
privateKey: string,
keyPassword?: string,
): Promise<any> {
try {
const response = await authApi.post("/credentials/detect-key-type", {
privateKey,
keyPassword,
});
return response.data;
} catch (error) {
throw handleApiError(error, "detect key type");
}
}
export async function detectPublicKeyType(
publicKey: string,
): Promise<any> {
try {
const response = await authApi.post("/credentials/detect-public-key-type", {
publicKey,
});
return response.data;
} catch (error) {
throw handleApiError(error, "detect public key type");
}
}
export async function validateKeyPair(
privateKey: string,
publicKey: string,
keyPassword?: string,
): Promise<any> {
try {
const response = await authApi.post("/credentials/validate-key-pair", {
privateKey,
publicKey,
keyPassword,
});
return response.data;
} catch (error) {
throw handleApiError(error, "validate key pair");
}
}