fix: Comprehensive ElGamal encryption system - DRY & KISS principles

Major improvements applying DRY (Don't Repeat Yourself) and KISS (Keep It Simple, Stupid):

BACKEND CHANGES:
- Fixed public key storage: Store as base64-encoded bytes in LargeBinary column (not double-encoding)
- ElGamal key generation now produces proper "p:g:h" format with colons
- Removed all double base64-encoding issues
- Simplified API responses to decode bytes to UTF-8 strings for JSON serialization

FRONTEND CHANGES:
- Refactored ElGamalEncryption.encrypt() to use extracted helper methods (_decodeBase64, _parsePublicKey)
- Eliminated nested error handling - now uses clear, composable private methods
- Improved error messages with specific format validation
- Simplified cryptographic operations by reducing code duplication

TESTING:
- Verified public key format: "p:g:h" properly encoded as base64
- Full vote submission flow tested and working
- Blockchain integration confirmed functional
- No encryption errors during vote submission

This fixes the original "Invalid public key format" error that was preventing vote submission.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
Alexis Bruneteau 2025-11-07 18:56:58 +01:00
parent 3efdabdbbd
commit c979f1a760
2 changed files with 52 additions and 61 deletions

View File

@ -499,15 +499,18 @@ async def setup_election(
# Générer les clés ElGamal si nécessaire # Générer les clés ElGamal si nécessaire
if not election.public_key: if not election.public_key:
elgamal = ElGamalEncryption(p=election.elgamal_p or 23, g=election.elgamal_g or 5) elgamal = ElGamal()
election.public_key = elgamal.public_key_bytes # Store as base64-encoded bytes (database column is LargeBinary)
# public_key_bytes returns UTF-8 "p:g:h", then encode to base64
election.public_key = base64.b64encode(elgamal.public_key_bytes)
db.add(election)
db.commit() db.commit()
return { return {
"status": "initialized", "status": "initialized",
"election_id": election_id, "election_id": election_id,
"public_keys": { "public_keys": {
"elgamal_pubkey": base64.b64encode(election.public_key).decode() if election.public_key else None "elgamal_pubkey": election.public_key.decode('utf-8') if election.public_key else None
}, },
"blockchain_blocks": blockchain.get_block_count() "blockchain_blocks": blockchain.get_block_count()
} }
@ -544,7 +547,7 @@ async def get_public_keys(
) )
return { return {
"elgamal_pubkey": base64.b64encode(election.public_key).decode() "elgamal_pubkey": election.public_key.decode('utf-8') if election.public_key else None
} }

View File

@ -70,69 +70,57 @@ export class ElGamalEncryption {
} }
try { try {
// Validate input // Decode base64: base64("p:g:h") → "p:g:h"
if (!publicKeyBase64 || typeof publicKeyBase64 !== "string") { const publicKeyStr = this._decodeBase64(publicKeyBase64);
throw new Error("Invalid public key: must be a non-empty string"); const [p, g, h] = this._parsePublicKey(publicKeyStr);
}
// Decode the base64 public key // Generate random r for encryption
// Format from backend: base64("p:g:h") where p, g, h are decimal numbers const r = BigInt(Math.floor(Math.random() * (Number(p) - 2)) + 1);
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 // ElGamal: (c1, c2) = (g^r mod p, m * h^r mod p)
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 c1 = this._modPow(g, r, p);
const c2 = (BigInt(vote) * this._modPow(h, r, p)) % p; const c2 = (BigInt(vote) * this._modPow(h, r, p)) % p;
// Return as "c1:c2" in base64 // Return encrypted as base64("c1:c2")
const encrypted = `${c1.toString()}:${c2.toString()}`; return btoa(`${c1}:${c2}`);
return btoa(encrypted);
} catch (error) { } catch (error) {
const errorMsg = error instanceof Error ? error.message : String(error || "Unknown error"); const msg = error instanceof Error ? error.message : String(error);
console.error("ElGamal encryption failed:", errorMsg); throw new Error(`ElGamal encryption failed: ${msg}`);
throw new Error(`Encryption failed: ${errorMsg}`); }
}
/**
* 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}`);
} }
} }