From 788499bcf28f67809f67fc80f84cd231e918a29e Mon Sep 17 00:00:00 2001 From: Antoine Jeanselme <67123340+ajeanselme@users.noreply.github.com> Date: Sat, 18 Apr 2026 08:48:31 +0200 Subject: [PATCH] Add ssh key generation button on sftp volume creation page --- .../components/volume-forms/sftp-form.tsx | 66 +++++++++++++++++++ 1 file changed, 66 insertions(+) diff --git a/app/client/modules/volumes/components/volume-forms/sftp-form.tsx b/app/client/modules/volumes/components/volume-forms/sftp-form.tsx index 869d9ead..77af19de 100644 --- a/app/client/modules/volumes/components/volume-forms/sftp-form.tsx +++ b/app/client/modules/volumes/components/volume-forms/sftp-form.tsx @@ -1,4 +1,5 @@ import { useWatch, type UseFormReturn } from "react-hook-form"; +import { useState } from "react"; import type { FormValues } from "../create-volume-form"; import { FormControl, @@ -12,13 +13,67 @@ import { Input } from "../../../../components/ui/input"; import { SecretInput } from "../../../../components/ui/secret-input"; import { Textarea } from "../../../../components/ui/textarea"; import { Switch } from "../../../../components/ui/switch"; +import { Button } from "~/client/components/ui/button"; type Props = { form: UseFormReturn; }; +const arrayBufferToBase64 = (buffer: ArrayBuffer) => { + let binary = ""; + const bytes = new Uint8Array(buffer); + const chunkSize = 0x8000; + + for (let i = 0; i < bytes.length; i += chunkSize) { + binary += String.fromCharCode(...bytes.subarray(i, i + chunkSize)); + } + + return btoa(binary); +}; + +const toPem = (base64: string, label: string) => { + const wrapped = base64.match(/.{1,64}/g)?.join("\n") ?? base64; + return `-----BEGIN ${label}-----\n${wrapped}\n-----END ${label}-----`; +}; + +const generatePrivateKeyPem = async () => { + const keyPair = await crypto.subtle.generateKey( + { + name: "RSASSA-PKCS1-v1_5", + modulusLength: 4096, + publicExponent: new Uint8Array([1, 0, 1]), + hash: "SHA-256", + }, + true, + ["sign", "verify"], + ); + + const privateKey = await crypto.subtle.exportKey("pkcs8", keyPair.privateKey); + return toPem(arrayBufferToBase64(privateKey), "OPENSSH PRIVATE KEY"); +}; + export const SFTPForm = ({ form }: Props) => { const skipHostKeyCheck = useWatch({ control: form.control, name: "skipHostKeyCheck" }); + const [isGeneratingKey, setIsGeneratingKey] = useState(false); + const [keyGenerationError, setKeyGenerationError] = useState(null); + + const handleGenerateKey = async () => { + setIsGeneratingKey(true); + setKeyGenerationError(null); + + try { + const privateKey = await generatePrivateKeyPem(); + form.setValue("privateKey", privateKey, { + shouldDirty: true, + shouldTouch: true, + shouldValidate: true, + }); + } catch { + setKeyGenerationError("Could not generate SSH key in this browser."); + } finally { + setIsGeneratingKey(false); + } + }; return ( <> @@ -100,6 +155,17 @@ export const SFTPForm = ({ form }: Props) => { SSH private key for authentication (optional if using password). + + The key is generated privately in your browser. Don't forget to save it! + {keyGenerationError && {keyGenerationError}} )} />