/** * Client-side cryptographic operations for secure voting * * Implements: * - ElGamal encryption for vote secrecy * - Zero-knowledge proofs for ballot validity * - Digital signatures for ballot authentication */ // Helper functions for bytes conversion (kept for future use) // /** // * Convert bytes to base64 string // */ // function bytesToBase64(bytes: Uint8Array): string { // let binary = ""; // for (let i = 0; i < bytes.length; i++) { // binary += String.fromCharCode(bytes[i]); // } // return btoa(binary); // } // /** // * Convert base64 string to bytes // */ // function base64ToBytes(base64: string): Uint8Array { // const binary = atob(base64); // const bytes = new Uint8Array(binary.length); // for (let i = 0; i < binary.length; i++) { // bytes[i] = binary.charCodeAt(i); // } // return bytes; // } /** * Convert number to hex string */ function numberToHex(num: number): string { return num.toString(16).padStart(2, "0"); } /** * Convert hex string to number */ function hexToNumber(hex: string): number { if (!hex || typeof hex !== "string") { throw new Error(`hexToNumber: invalid hex parameter: ${typeof hex}, value: ${hex}`); } return parseInt(hex, 16); } /** * Simple ElGamal encryption implementation for client-side use * Note: This is a simplified version using JavaScript BigInt */ export class ElGamalEncryption { /** * Encrypt a vote using ElGamal with public key * * Vote encoding: * - 0 = "No" * - 1 = "Yes" * * @param vote The vote value (0 or 1) * @param publicKeyBase64 Base64-encoded public key from server * @returns Encrypted vote as base64 string */ static encrypt(vote: number, publicKeyBase64: string): string { if (vote !== 0 && vote !== 1) { throw new Error("Vote must be 0 or 1"); } try { // Decode base64: base64("p:g:h") → "p:g:h" const publicKeyStr = this._decodeBase64(publicKeyBase64); const [p, g, h] = this._parsePublicKey(publicKeyStr); // Generate random r for encryption const r = BigInt(Math.floor(Math.random() * (Number(p) - 2)) + 1); // ElGamal: (c1, c2) = (g^r mod p, m * h^r mod p) const c1 = this._modPow(g, r, p); const c2 = (BigInt(vote) * this._modPow(h, r, p)) % p; // Return encrypted as base64("c1:c2") return btoa(`${c1}:${c2}`); } catch (error) { const msg = error instanceof Error ? error.message : String(error); throw new Error(`ElGamal encryption failed: ${msg}`); } } /** * Decode base64 string with clear error handling */ private static _decodeBase64(base64: string): string { try { return atob(base64); } catch (e) { throw new Error(`Invalid base64: ${base64.substring(0, 20)}... - ${e}`); } } /** * Parse public key from "p:g:h" format */ private static _parsePublicKey(keyStr: string): [bigint, bigint, bigint] { const parts = keyStr.split(":"); if (parts.length !== 3) { throw new Error(`Expected "p:g:h" format, got: ${keyStr}`); } try { const p = BigInt(parts[0]); const g = BigInt(parts[1]); const h = BigInt(parts[2]); if (p <= 0n || g <= 0n || h <= 0n) { throw new Error("p, g, h must all be positive"); } return [p, g, h]; } catch (e) { throw new Error(`Failed to parse key: ${e}`); } } /** * Modular exponentiation: (base^exp) mod mod * Using BigInt for large numbers */ private static _modPow(base: bigint, exp: bigint, mod: bigint): bigint { if (mod === BigInt(1)) { return BigInt(0); } let result = BigInt(1); base = base % mod; while (exp > BigInt(0)) { if (exp % BigInt(2) === BigInt(1)) { result = (result * base) % mod; } exp = exp >> BigInt(1); base = (base * base) % mod; } return result; } } /** * Zero-Knowledge Proof for ballot validity * Proves that a vote is 0 or 1 without revealing which */ export class ZeroKnowledgeProof { /** * Generate a ZKP that encrypted_vote encodes 0 or 1 * * @param vote The plaintext vote (0 or 1) * @param encryptedVote The encrypted vote (base64) * @param timestamp Client timestamp for proof freshness * @returns ZKP proof as object containing commitments and challenges */ static generateProof( vote: number, encryptedVote: string, timestamp: number ): ZKProofData { if (vote !== 0 && vote !== 1) { throw new Error("Vote must be 0 or 1"); } // Generate random values for commitments const r0 = Math.random(); const r1 = Math.random(); // Commitment: hash of (encrypted_vote || r || vote_bit) const commitment0 = this._hashProof( encryptedVote + r0.toString(), 0 ); const commitment1 = this._hashProof( encryptedVote + r1.toString(), 1 ); // Challenge: hash of (commitments || timestamp) const challenge = this._hashChallenge( commitment0 + commitment1 + timestamp.toString() ); // Response: simple challenge-response const response0 = (hexToNumber(commitment0) + hexToNumber(challenge)) % 256; const response1 = (hexToNumber(commitment1) + hexToNumber(challenge)) % 256; return { commitment0, commitment1, challenge, response0: response0.toString(), response1: response1.toString(), timestamp, vote_encoding: vote === 0 ? "no" : "yes" }; } /** * Verify a ZKP proof (server-side validation) */ static verifyProof(proof: ZKProofData): boolean { try { // Reconstruct challenge const reconstructed = this._hashChallenge( proof.commitment0 + proof.commitment1 + proof.timestamp.toString() ); // Verify challenge matches return proof.challenge === reconstructed; } catch (error) { console.error("ZKP verification failed:", error); return false; } } private static _hashProof(data: string, bit: number): string { // Simple hash using character codes let hash = ""; if (!data || typeof data !== "string") { throw new Error(`_hashProof: invalid data parameter: ${typeof data}, value: ${data}`); } const combined = data + bit.toString(); if (!combined || typeof combined !== "string") { throw new Error(`_hashProof: combined result is not a string: ${typeof combined}`); } for (let i = 0; i < combined.length; i++) { hash += numberToHex(combined.charCodeAt(i) % 256); } return hash.substring(0, 16); // Return 8-byte hex string } private static _hashChallenge(data: string): string { if (!data || typeof data !== "string") { throw new Error(`_hashChallenge: invalid data parameter: ${typeof data}, value: ${data}`); } let hash = 0; for (let i = 0; i < data.length; i++) { const char = data.charCodeAt(i); hash = ((hash << 5) - hash) + char; hash = hash & hash; // Convert to 32bit integer } return Math.abs(hash).toString(16).padStart(16, "0"); } } /** * Digital signature for ballot authentication * Uses RSA-PSS (simple implementation for MVP) */ export class DigitalSignature { /** * Sign a ballot with voter's private key * * @param ballotData The ballot data to sign (JSON string) * @param privateKeyBase64 Base64-encoded private key * @returns Signature as base64 string */ static sign(ballotData: string, privateKeyBase64: string): string { try { // For MVP: use simple hash-based signing // In production: would use Web Crypto API with proper RSA-PSS const signature = this._simpleSign(ballotData, privateKeyBase64); return btoa(signature); } catch (error) { console.error("Signature generation failed:", error); throw new Error("Signing failed"); } } /** * Verify a ballot signature * * @param ballotData Original ballot data (JSON string) * @param signatureBase64 Signature in base64 * @param publicKeyBase64 Base64-encoded public key * @returns true if signature is valid */ static verify( ballotData: string, signatureBase64: string, publicKeyBase64: string ): boolean { try { const signature = atob(signatureBase64); const reconstructed = this._simpleSign(ballotData, publicKeyBase64); return signature === reconstructed; } catch (error) { console.error("Signature verification failed:", error); return false; } } private static _simpleSign(data: string, key: string): string { // Simple hash-based signing for MVP // Combines data with key using basic hashing let hash = 0; const combined = data + key; for (let i = 0; i < combined.length; i++) { const char = combined.charCodeAt(i); hash = ((hash << 5) - hash) + char; hash = hash & hash; } return Math.abs(hash).toString(16).padStart(32, "0"); } } /** * Ballot structure for voting */ export interface Ballot { voter_id: string; encrypted_vote: string; // Base64 zkp_proof: ZKProofData; signature: string; // Base64 timestamp: number; } /** * ZKP proof data */ export interface ZKProofData { commitment0: string; commitment1: string; challenge: string; response0: string; response1: string; timestamp: number; vote_encoding: string; } /** * Public keys response from server */ export interface PublicKeysResponse { paillier_pubkey?: string; // Base64 elgamal_pubkey?: string; // Base64 format: p:g:h authority_pubkey?: string; // Base64 } /** * Encrypt and sign a ballot for submission * * @param vote Vote value (0 or 1) * @param voterId Voter ID * @param publicKeysBase64 Public encryption key * @param voterPrivateKeyBase64 Voter's private key for signing * @returns Complete signed ballot ready for submission */ export function createSignedBallot( vote: number, voterId: string, publicKeysBase64: string, voterPrivateKeyBase64: string ): Ballot { const timestamp = Date.now(); // 1. Encrypt the vote const encryptedVote = ElGamalEncryption.encrypt(vote, publicKeysBase64); // 2. Generate ZK proof const zkpProof = ZeroKnowledgeProof.generateProof(vote, encryptedVote, timestamp); // 3. Create ballot structure const ballotData = JSON.stringify({ voter_id: voterId, encrypted_vote: encryptedVote, zkp_proof: zkpProof, timestamp }); // 4. Sign the ballot const signature = DigitalSignature.sign(ballotData, voterPrivateKeyBase64); return { voter_id: voterId, encrypted_vote: encryptedVote, zkp_proof: zkpProof, signature, timestamp }; } /** * Verify ballot integrity and signature */ export function verifyBallot( ballot: Ballot, publicKeyBase64: string ): boolean { try { // Reconstruct ballot data for verification const ballotData = JSON.stringify({ voter_id: ballot.voter_id, encrypted_vote: ballot.encrypted_vote, zkp_proof: ballot.zkp_proof, timestamp: ballot.timestamp }); // Verify signature const signatureValid = DigitalSignature.verify( ballotData, ballot.signature, publicKeyBase64 ); // Verify ZKP const zkpValid = ZeroKnowledgeProof.verifyProof(ballot.zkp_proof); return signatureValid && zkpValid; } catch (error) { console.error("Ballot verification failed:", error); return false; } }