Add SSH key generation and deployment features #234

Merged
ZacharyZcR merged 6 commits from main into dev-1.7.0 2025-09-15 02:29:32 +00:00
2 changed files with 45 additions and 12 deletions
Showing only changes of commit 1ac96e7c74 - Show all commits

View File

@@ -11,7 +11,7 @@ import ssh2Pkg from "ssh2";
const { utils: ssh2Utils } = ssh2Pkg;
// Direct SSH key generation with ssh2 - the right way
function generateSSHKeyPair(keyType: string, keySize?: number): { success: boolean; privateKey?: string; publicKey?: string; error?: string } {
function generateSSHKeyPair(keyType: string, keySize?: number, passphrase?: string): { success: boolean; privateKey?: string; publicKey?: string; error?: string } {
console.log('Generating SSH key pair with ssh2:', keyType);
try {
@@ -29,6 +29,12 @@ function generateSSHKeyPair(keyType: string, keySize?: number): { success: boole
options.bits = 256; // ECDSA P-256 uses 256 bits
}
// Add passphrase protection if provided
if (passphrase && passphrase.trim()) {
options.passphrase = passphrase;
options.cipher = 'aes128-cbc'; // Default cipher for encrypted private keys
}
coderabbitai[bot] commented 2025-09-14 23:13:14 +00:00 (Migrated from github.com)
Review

🛠️ Refactor suggestion

⚠️ Potential issue

Weak/default crypto options in key generation.

  • For passphrase-protected keys, don’t force aes128-cbc. Prefer ssh2 defaults (bcrypt KDF + aes256-ctr) or explicitly set stronger cipher.
- if (passphrase && passphrase.trim()) {
-   options.passphrase = passphrase;
-   options.cipher = 'aes128-cbc';
- }
+ if (passphrase && passphrase.trim()) {
+   options.passphrase = passphrase;
+   // rely on ssh2's modern defaults (OpenSSH new-format with bcrypt KDF)
+ }

Also consider exposing ECDSA curves via options.curve instead of bits for clarity.

Also applies to: 38-50

_🛠️ Refactor suggestion_ _⚠️ Potential issue_ **Weak/default crypto options in key generation.** - For passphrase-protected keys, don’t force aes128-cbc. Prefer ssh2 defaults (bcrypt KDF + aes256-ctr) or explicitly set stronger cipher. ```diff - if (passphrase && passphrase.trim()) { - options.passphrase = passphrase; - options.cipher = 'aes128-cbc'; - } + if (passphrase && passphrase.trim()) { + options.passphrase = passphrase; + // rely on ssh2's modern defaults (OpenSSH new-format with bcrypt KDF) + } ``` Also consider exposing ECDSA curves via options.curve instead of bits for clarity. Also applies to: 38-50 <!-- fingerprinting:phantom:medusa:chinchilla --> <!-- This is an auto-generated comment by CodeRabbit -->
// Use ssh2's native key generation
const keyPair = ssh2Utils.generateKeyPairSync(ssh2Type as any, options);
@@ -882,8 +888,8 @@ router.post("/generate-key-pair", authenticateJWT, async (req: Request, res: Res
console.log("Has passphrase:", !!passphrase);
try {
// Generate keys with crypto, convert public key to SSH format
const result = generateSSHKeyPair(keyType, keySize);
// Generate SSH keys directly with ssh2
const result = generateSSHKeyPair(keyType, keySize, passphrase);
if (result.success && result.privateKey && result.publicKey) {
const response = {

View File

@@ -376,6 +376,7 @@ export function CredentialEditor({
};
const [tagInput, setTagInput] = useState("");
const [keyGenerationPassphrase, setKeyGenerationPassphrase] = useState("");
const [folderDropdownOpen, setFolderDropdownOpen] = useState(false);
const folderInputRef = useRef<HTMLInputElement>(null);
@@ -675,6 +676,23 @@ export function CredentialEditor({
<FormLabel className="mb-3 font-bold block">
{t("credentials.generateKeyPair")}
</FormLabel>
{/* Key Generation Passphrase Input */}
<div className="mb-3">
<FormLabel className="text-sm mb-2 block">
{t("credentials.keyPassword")} ({t("credentials.optional")})
</FormLabel>
<PasswordInput
placeholder={t("placeholders.keyPassword")}
value={keyGenerationPassphrase}
onChange={(e) => setKeyGenerationPassphrase(e.target.value)}
className="max-w-xs"
/>
<div className="text-xs text-muted-foreground mt-1">
{t("credentials.keyPassphraseOptional")}
</div>
</div>
<div className="flex gap-2 flex-wrap">
<Button
type="button"
@@ -682,13 +700,16 @@ export function CredentialEditor({
size="sm"
onClick={async () => {
try {
const keyPassword = form.watch("keyPassword");
const result = await generateKeyPair('ssh-ed25519', undefined, keyPassword);
const result = await generateKeyPair('ssh-ed25519', undefined, keyGenerationPassphrase);
if (result.success) {
form.setValue("key", result.privateKey);
form.setValue("publicKey", result.publicKey);
debouncedKeyDetection(result.privateKey, keyPassword);
// Auto-fill the key password field if passphrase was used
if (keyGenerationPassphrase) {
form.setValue("keyPassword", keyGenerationPassphrase);
}
debouncedKeyDetection(result.privateKey, keyGenerationPassphrase);
debouncedPublicKeyDetection(result.publicKey);
toast.success(t("credentials.keyPairGeneratedSuccessfully", { keyType: "Ed25519" }));
} else {
@@ -708,13 +729,16 @@ export function CredentialEditor({
size="sm"
onClick={async () => {
try {
const keyPassword = form.watch("keyPassword");
const result = await generateKeyPair('ecdsa-sha2-nistp256', undefined, keyPassword);
const result = await generateKeyPair('ecdsa-sha2-nistp256', undefined, keyGenerationPassphrase);
if (result.success) {
form.setValue("key", result.privateKey);
form.setValue("publicKey", result.publicKey);
debouncedKeyDetection(result.privateKey, keyPassword);
// Auto-fill the key password field if passphrase was used
if (keyGenerationPassphrase) {
form.setValue("keyPassword", keyGenerationPassphrase);
}
debouncedKeyDetection(result.privateKey, keyGenerationPassphrase);
debouncedPublicKeyDetection(result.publicKey);
toast.success(t("credentials.keyPairGeneratedSuccessfully", { keyType: "ECDSA" }));
} else {
@@ -734,13 +758,16 @@ export function CredentialEditor({
size="sm"
onClick={async () => {
try {
const keyPassword = form.watch("keyPassword");
const result = await generateKeyPair('ssh-rsa', 2048, keyPassword);
const result = await generateKeyPair('ssh-rsa', 2048, keyGenerationPassphrase);
if (result.success) {
form.setValue("key", result.privateKey);
form.setValue("publicKey", result.publicKey);
debouncedKeyDetection(result.privateKey, keyPassword);
// Auto-fill the key password field if passphrase was used
if (keyGenerationPassphrase) {
form.setValue("keyPassword", keyGenerationPassphrase);
}
debouncedKeyDetection(result.privateKey, keyGenerationPassphrase);
debouncedPublicKeyDetection(result.publicKey);
toast.success(t("credentials.keyPairGeneratedSuccessfully", { keyType: "RSA" }));
} else {