- Fixed ElGamal class instantiation in votes.py (ElGamalEncryption instead of ElGamal) - Fixed public key serialization in admin.py (use public_key_bytes property) - Implemented database migration with SQL-based key generation - Added vote deduplication endpoint: GET /api/votes/check - Protected all array accesses with type validation in frontend - Fixed vote parameter type handling (string to number conversion) - Removed all debug console logs for production - Created missing dynamic route for vote history details Fixes: - JavaScript error: "can't access property length, e is undefined" - Vote deduplication not preventing form display - Frontend data validation issues - Missing dynamic routes
439 lines
12 KiB
TypeScript
439 lines
12 KiB
TypeScript
/**
|
|
* 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 {
|
|
// Validate input
|
|
if (!publicKeyBase64 || typeof publicKeyBase64 !== "string") {
|
|
throw new Error("Invalid public key: must be a non-empty string");
|
|
}
|
|
|
|
// Decode the base64 public key
|
|
// Format from backend: base64("p:g:h") where p, g, h are decimal numbers
|
|
let publicKeyStr: string;
|
|
try {
|
|
publicKeyStr = atob(publicKeyBase64);
|
|
} catch (e) {
|
|
const errorMsg = e instanceof Error ? e.message : String(e);
|
|
throw new Error(`Failed to decode public key from base64: ${errorMsg}`);
|
|
}
|
|
|
|
// Validate decoded string
|
|
if (!publicKeyStr || typeof publicKeyStr !== "string") {
|
|
throw new Error("Invalid decoded public key: must be a non-empty string");
|
|
}
|
|
|
|
// Parse public key (format: p:g:h separated by colons)
|
|
const publicKeyData = publicKeyStr.split(":");
|
|
|
|
if (publicKeyData.length < 3) {
|
|
throw new Error(
|
|
`Invalid public key format. Expected "p:g:h" but got "${publicKeyStr}"`
|
|
);
|
|
}
|
|
|
|
// Parse and validate each component
|
|
let p: bigint, g: bigint, h: bigint;
|
|
try {
|
|
p = BigInt(publicKeyData[0]); // Prime
|
|
g = BigInt(publicKeyData[1]); // Generator
|
|
h = BigInt(publicKeyData[2]); // Public key = g^x mod p
|
|
} catch (e) {
|
|
const errorMsg = e instanceof Error ? e.message : String(e);
|
|
throw new Error(`Failed to parse public key numbers: ${errorMsg}`);
|
|
}
|
|
|
|
// Validate parameters
|
|
if (p <= 0n || g <= 0n || h <= 0n) {
|
|
throw new Error("Invalid public key parameters: p, g, h must be positive");
|
|
}
|
|
|
|
// Generate random number for encryption
|
|
const maxRandom = Number(p) - 2;
|
|
if (maxRandom <= 0) {
|
|
throw new Error("Public key prime p is too small");
|
|
}
|
|
const r = BigInt(Math.floor(Math.random() * maxRandom) + 1);
|
|
|
|
// ElGamal encryption: (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 as "c1:c2" in base64
|
|
const encrypted = `${c1.toString()}:${c2.toString()}`;
|
|
return btoa(encrypted);
|
|
} catch (error) {
|
|
const errorMsg = error instanceof Error ? error.message : String(error || "Unknown error");
|
|
console.error("ElGamal encryption failed:", errorMsg);
|
|
throw new Error(`Encryption failed: ${errorMsg}`);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 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;
|
|
}
|
|
}
|