Skip to content

Commit

Permalink
feat: Implement asymmetric encryption for signing Val.town requests
Browse files Browse the repository at this point in the history
  • Loading branch information
nadeesha committed Dec 30, 2024
1 parent 4b0044f commit d2cfaa2
Show file tree
Hide file tree
Showing 10 changed files with 704 additions and 76 deletions.
167 changes: 112 additions & 55 deletions app/app/clusters/[clusterId]/integrations/valtown/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,30 +9,19 @@ import {
CardHeader,
CardTitle,
} from "@/components/ui/card";
import {
Form,
FormControl,
FormDescription,
FormField,
FormItem,
FormLabel,
FormMessage,
} from "@/components/ui/form";
import { Input } from "@/components/ui/input";
import { createErrorToast } from "@/lib/utils";
import { useAuth } from "@clerk/nextjs";
import { zodResolver } from "@hookform/resolvers/zod";
import { useCallback, useEffect, useState } from "react";
import { useForm } from "react-hook-form";
import toast from "react-hot-toast";
import { z } from "zod";
import { ArrowLeft } from "lucide-react";
import { ArrowLeft, ClipboardCopy } from "lucide-react";
import Link from "next/link";
import { Loading } from "@/components/loading";

const formSchema = z.object({
endpoint: z.string().url("Please enter a valid URL").min(1, "Endpoint URL is required"),
});
interface CryptoKeyPair {
publicKey: CryptoKey;
privateKey: CryptoKey;
}

export default function ValtownIntegration({
params: { clusterId },
Expand All @@ -41,16 +30,75 @@ export default function ValtownIntegration({
}) {
const { getToken } = useAuth();
const [loading, setLoading] = useState(false);
const [publicKey, setPublicKey] = useState<string | null>(null);
const [endpoint, setEndpoint] = useState("");
const [error, setError] = useState<string | null>(null);

const generateKeyPair = useCallback(async () => {
const loadingToast = toast.loading("Generating key pair...");
try {
const algorithm: RsaHashedKeyGenParams = {
name: "RSASSA-PKCS1-v1_5",
modulusLength: 2048,
publicExponent: new Uint8Array([1, 0, 1]),
hash: { name: "SHA-256" },
};

const keyPair = await window.crypto.subtle.generateKey(
algorithm,
true,
["sign"]
) as CryptoKeyPair;

const publicKey = await window.crypto.subtle.exportKey(
"spki",
keyPair.publicKey
);
const privateKey = await window.crypto.subtle.exportKey(
"pkcs8",
keyPair.privateKey
);

// Convert to base64
const publicKeyBase64 = btoa(Array.from(new Uint8Array(publicKey))
.map(b => String.fromCharCode(b))
.join(""));
const privateKeyBase64 = btoa(Array.from(new Uint8Array(privateKey))
.map(b => String.fromCharCode(b))
.join(""));

toast.dismiss(loadingToast);
toast.success("Key pair generated successfully");
setPublicKey(publicKeyBase64);
return { publicKey: publicKeyBase64, privateKey: privateKeyBase64 };
} catch (error) {
toast.dismiss(loadingToast);
toast.error("Failed to generate key pair");
console.error("Key generation error:", error);
return null;
}
}, []);

const handleSubmit = async () => {
setError(null);

const form = useForm<z.infer<typeof formSchema>>({
resolver: zodResolver(formSchema),
defaultValues: {
endpoint: "",
},
});
// Basic URL validation
try {
new URL(endpoint);
} catch {
setError("Please enter a valid URL");
return;
}

const onSubmit = async (data: z.infer<typeof formSchema>) => {
const loadingToast = toast.loading("Saving configuration...");

// Generate key pair first
const keys = await generateKeyPair();
if (!keys) {
toast.dismiss(loadingToast);
return;
}

const response = await client.upsertIntegrations({
headers: {
authorization: `Bearer ${await getToken()}`,
Expand All @@ -60,7 +108,9 @@ export default function ValtownIntegration({
},
body: {
valtown: {
endpoint: data.endpoint,
endpoint,
publicKey: keys.publicKey,
privateKey: keys.privateKey,
},
},
});
Expand All @@ -87,18 +137,11 @@ export default function ValtownIntegration({
});
setLoading(false);

if (response.status === 200) {
const result = z
.object({
endpoint: z.string(),
})
.safeParse(response.body?.valtown);

if (result.success) {
form.setValue("endpoint", result.data.endpoint);
}
if (response.status === 200 && response.body?.valtown) {
setEndpoint(response.body.valtown.endpoint || "");
setPublicKey(response.body.valtown.publicKey || null);
}
}, [clusterId, getToken, form]);
}, [clusterId, getToken]);

useEffect(() => {
fetchConfig();
Expand Down Expand Up @@ -132,27 +175,41 @@ export default function ValtownIntegration({
</CardDescription>
</CardHeader>
<CardContent>
<Form {...form}>
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-4">
<FormField
control={form.control}
name="endpoint"
render={({ field }) => (
<FormItem>
<FormLabel>Val Endpoint URL</FormLabel>
<FormControl>
<Input {...field} placeholder="https://myval.web.val.run" />
</FormControl>
<FormDescription>
The URL endpoint of your Val that will be called for processing
</FormDescription>
<FormMessage />
</FormItem>
)}
<div className="space-y-4">
<div>
<label className="text-sm font-medium">Val Endpoint URL</label>
<Input
value={endpoint}
onChange={(e) => setEndpoint(e.target.value)}
placeholder="https://myval.web.val.run"
/>
<Button type="submit">Save Configuration</Button>
</form>
</Form>
<p className="text-sm text-gray-500 mt-1">
The URL endpoint of your Val that will be called for processing
</p>
{error && <p className="text-sm text-red-500 mt-1">{error}</p>}
</div>
<Button onClick={handleSubmit}>Save Configuration</Button>
</div>

{publicKey && (
<div className="mt-6 space-y-4">
<div>
<label className="text-sm font-medium">Generated Public Key</label>
<textarea
readOnly
className="w-full min-h-[100px] p-2 text-sm font-mono border rounded-md"
value={publicKey}
/>
<p className="text-sm text-gray-500 mt-1">
This public key is used to verify message integrity. Share this key with your Val to enable it to verify that requests are genuinely coming from your Inferable cluster.
</p>
</div>
<Button variant="outline" onClick={() => {
navigator.clipboard.writeText(publicKey);
toast.success("Copied to clipboard");
}}><ClipboardCopy className="w-4 h-4 mr-2" /> Copy Public Key</Button>
</div>
)}
</CardContent>
</Card>
</div>
Expand Down
2 changes: 2 additions & 0 deletions app/client/contract.ts
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,8 @@ export const integrationSchema = z.object({
valtown: z
.object({
endpoint: z.string().url(),
publicKey: z.string(),
privateKey: z.string(),
})
.optional()
.nullable(),
Expand Down
Loading

0 comments on commit d2cfaa2

Please sign in to comment.