Implement Enterprise-Grade Database Encryption System #244
@@ -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")
|
||||
|
||||
@@ -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));
|
||||
|
|
||||
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;
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -0,0 +1,444 @@
|
||||
// Simple fallback SSH key type detection
|
||||
|
⚠️ Potential issue Critical: Binary string conversion loses data integrity Converting Buffer to 'binary' string can corrupt the data due to character encoding issues. This could lead to incorrect key type detection. Then search for the key type indicators directly in the Buffer: 📝 Committable suggestion
🤖 Prompt for AI Agents_⚠️ Potential issue_
**Critical: Binary string conversion 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 -->
⚠️ Potential issue Critical: Binary string conversion issue repeated Same binary string conversion issue that can corrupt data during PKCS#8 format detection. Search for OIDs directly in the Buffer: 📝 Committable suggestion
🤖 Prompt for AI Agents_⚠️ Potential issue_
**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 -->
⚠️ Potential issue Unreliable key type detection based on content length Using content length to determine key type is highly unreliable and could lead to incorrect key type identification. Key sizes can vary significantly within the same key type. Consider removing this fallback or implementing a more robust detection mechanism: 📝 Committable suggestion
🤖 Prompt for AI Agents_⚠️ Potential issue_
**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 -->
⚠️ Potential issue Critical: Binary string conversion in public key detection Same issue with binary string conversion that can corrupt data during public key type detection.
🤖 Prompt for AI Agents_⚠️ Potential issue_
**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 -->
⚠️ Potential issue Unreliable public key type detection based on length Using content length for public key type detection is unreliable and error-prone. 📝 Committable suggestion
_⚠️ Potential issue_
**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 -->
⚠️ Potential issue Missing passphrase parameter in detectKeyType function The 📝 Committable suggestion
🤖 Prompt for AI Agents_⚠️ 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 -->
⚠️ Potential issue Misleading validation result when unable to verify Returning 📝 Committable suggestion
🤖 Prompt for AI Agents_⚠️ 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,30 +788,48 @@ export function CredentialEditor({
|
||||
</TabsTrigger>
|
||||
</TabsList>
|
||||
<TabsContent value="upload" className="mt-4">
|
||||
<div className="grid grid-cols-2 gap-4 items-start">
|
||||
<Controller
|
||||
control={form.control}
|
||||
name="key"
|
||||
render={({ field }) => (
|
||||
<FormItem className="mb-4">
|
||||
<FormLabel>
|
||||
<FormItem className="mb-4 flex flex-col">
|
||||
<FormLabel className="mb-2 min-h-[20px]">
|
||||
{t("credentials.sshPrivateKey")}
|
||||
</FormLabel>
|
||||
<FormControl>
|
||||
<div className="relative inline-block">
|
||||
<div className="relative inline-block w-full">
|
||||
<input
|
||||
id="key-upload"
|
||||
type="file"
|
||||
accept=".pem,.key,.txt,.ppk"
|
||||
onChange={(e) => {
|
||||
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="justify-start text-left"
|
||||
className="w-full justify-start text-left"
|
||||
>
|
||||
<span
|
||||
className="truncate"
|
||||
@@ -668,9 +849,98 @@ export function CredentialEditor({
|
||||
</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>
|
||||
{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>
|
||||
{/* 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,12 +1039,13 @@ export function CredentialEditor({
|
||||
</div>
|
||||
</TabsContent>
|
||||
<TabsContent value="paste" className="mt-4">
|
||||
<div className="grid grid-cols-2 gap-4 items-start">
|
||||
<Controller
|
||||
control={form.control}
|
||||
name="key"
|
||||
render={({ field }) => (
|
||||
<FormItem className="mb-4">
|
||||
<FormLabel>
|
||||
<FormItem className="mb-4 flex flex-col">
|
||||
<FormLabel className="mb-2 min-h-[20px]">
|
||||
{t("credentials.sshPrivateKey")}
|
||||
</FormLabel>
|
||||
<FormControl>
|
||||
@@ -779,14 +1059,110 @@ export function CredentialEditor({
|
||||
? field.value
|
||||
: ""
|
||||
}
|
||||
onChange={(e) =>
|
||||
field.onChange(e.target.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}
|
||||
|
||||
@@ -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");
|
||||
}
|
||||
}
|
||||
|
||||
⚠️ 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:
📝 Committable suggestion
🤖 Prompt for AI Agents
⚠️ 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.
📝 Committable suggestion
🤖 Prompt for AI Agents
⚠️ 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:
🤖 Prompt for AI Agents