// utils/crypto.ts export const generateUserKeyPair = async () => { return await window.crypto.subtle.generateKey( { name: "RSASSA-PKCS1-v1_5", modulusLength: 2048, publicExponent: new Uint8Array([1, 0, 1]), // 65537 hash: "SHA-256", }, true, // extractable ["sign", "verify"] ); }; export const signVote = async (data: any, privateKey: CryptoKey) => { const encoder = new TextEncoder(); const encodedData = encoder.encode(JSON.stringify(data)); const signature = await window.crypto.subtle.sign( "RSASSA-PKCS1-v1_5", privateKey, encodedData ); // Convert to Base64 or Hex to store in Yjs easily return btoa(String.fromCharCode(...new Uint8Array(signature))); }; export const verifyVote = async (data: any, signatureStr: string, publicKey: CryptoKey) => { const encoder = new TextEncoder(); const encodedData = encoder.encode(JSON.stringify(data)); // Convert Base64 back to Uint8Array const signature = Uint8Array.from(atob(signatureStr), c => c.charCodeAt(0)); return await window.crypto.subtle.verify( "RSASSA-PKCS1-v1_5", publicKey, signature, encodedData ); }; /** * Verifies a specific vote within an array of votes by * reconstructing the "signed state" at that point in time. */ export const verifyChainedVote = async ( voteData: SignedData[], index: number ) => { const voteToVerify = voteData[index]; console.log("Verifying vote: " + voteToVerify) if(voteToVerify) { // 1. Reconstruct the exact data state the user signed // We need the array exactly as it was when they pushed their vote const historicalState = voteData.slice(0, index + 1).map((v, i) => { if (i === index) { // For the current vote, the signature must be empty string // because it wasn't signed yet when passed to signVote return { ...v, signature: "" }; } return v; }); try { // 2. Fetch public key const response = await $fetch<{ public_key: string }>(`/api/users/${voteToVerify.data.userid}`); console.log("Got key: ",response) const pubKey = await stringToCryptoKey(response.public_key, 'public'); console.log("Using pubKey to verify Vote.") // 3. Verify: Does this historicalState match the signature? return await verifyVote(historicalState, voteToVerify.signature, pubKey); } catch (err) { console.error("Verification failed") console.error(err); return false; } } console.error("Vote is undefined or null"); return false; }; export const verifyAllVotesForOption = async (votes: SignedData[]) => { console.log("verifying votes for option ",votes); for (let i = votes.length-1; i >= 0 ; i--) { const isValid = await verifyChainedVote(votes, i); if(!isValid){ console.error("Error! Invalid Vote at: " + i,votes) return false; } } return true; }; // Helper to convert ArrayBuffer to Base64 string const bufferToBase64 = (buf: ArrayBuffer) => window.btoa(String.fromCharCode(...new Uint8Array(buf))); export const exportPublicKey = async (key: CryptoKey) => { // Export Public Key const exportedPublic = await window.crypto.subtle.exportKey("spki", key); const publicKeyString = bufferToBase64(exportedPublic); return publicKeyString; }; export const exportPrivateKey = async (key: CryptoKey) => { // Export Private Key const exportedPrivate = await window.crypto.subtle.exportKey("pkcs8", key); const privateKeyString = bufferToBase64(exportedPrivate); return privateKeyString; }; /** * Converts a Base64 string back into a usable CryptoKey object * @param keyStr The Base64 string (without PEM headers) * @param type 'public' or 'private' */ export const stringToCryptoKey = async (keyStr: string, type: 'public' | 'private'): Promise => { // 1. Convert Base64 string to a Uint8Array (binary) const binaryString = window.atob(keyStr); const bytes = new Uint8Array(binaryString.length); for (let i = 0; i < binaryString.length; i++) { bytes[i] = binaryString.charCodeAt(i); } // 2. Identify the format based on the key type // Public keys usually use 'spki', Private keys use 'pkcs8' const format = type === 'public' ? 'spki' : 'pkcs8'; const usages: KeyUsage[] = type === 'public' ? ['verify'] : ['sign']; // 3. Import the key return await window.crypto.subtle.importKey( format, bytes.buffer, { name: "RSASSA-PKCS1-v1_5", hash: "SHA-256", }, true, // extractable (set to false if you want to lock it in memory) usages ); }; export const savePrivateKeyToFile = (privateKeyStr: string, filename: string) => { // Optional: Wrap in PEM headers for standard formatting const pemHeader = "-----BEGIN PRIVATE KEY-----\n"; const pemFooter = "\n-----END PRIVATE KEY-----"; const fileContent = pemHeader + privateKeyStr + pemFooter; const blob = new Blob([fileContent], { type: "text/plain" }); const url = URL.createObjectURL(blob); const link = document.createElement("a"); link.href = url; link.download = filename; document.body.appendChild(link); link.click(); // Cleanup document.body.removeChild(link); URL.revokeObjectURL(url); }; export const loadPrivateKeyFromFile = async (file: File): Promise => { return new Promise((resolve, reject) => { const reader = new FileReader(); reader.onload = (e) => { const content = e.target?.result as string; // Clean up the string by removing PEM headers and newlines const cleanKey = content .replace("-----BEGIN PRIVATE KEY-----", "") .replace("-----END PRIVATE KEY-----", "") .replace(/\s+/g, ""); // Removes all whitespace/newlines resolve(cleanKey); }; reader.onerror = () => reject("Error reading file"); reader.readAsText(file); }); };