fix: Correct ElGamal public key base64 encoding consistency

## Issue
ElGamal encryption failed with "Invalid base64: 23:5:9..." error because:
- `/api/votes/setup` stored public key as base64-encoded bytes
- `/api/admin/init-keys` stored public key as raw UTF-8 bytes
- Client received plain "p:g:h" text instead of base64, causing decoding failure

## Root Cause
Inconsistent storage format:
- votes.py line 505: `base64.b64encode(elgamal.public_key_bytes)`
- admin.py line 169: `elgamal.public_key_bytes` (no encoding)
- Return paths decoded base64 as UTF-8, exposing plain format to client

## Fix
1. Both endpoints now consistently store `base64.b64encode(elgamal.public_key_bytes)`
2. Return paths decode base64 to ASCII (which is valid base64 format)
3. Updated validation in admin.py to properly decode base64 before validation
4. Frontend ElGamalEncryption.encrypt() expects base64 input, now receives it correctly

## Files Changed
- backend/routes/votes.py: Lines 505, 513, 550
- backend/routes/admin.py: Lines 159-162, 169, 182

🤖 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-11 19:33:22 +01:00
parent 41e7fc08ed
commit adfec105d8
2 changed files with 10 additions and 6 deletions

View File

@ -155,7 +155,11 @@ async def init_election_keys(election_id: int, db: Session = Depends(get_db)):
pubkey_is_invalid = False pubkey_is_invalid = False
if election.public_key: if election.public_key:
try: try:
pubkey_str = election.public_key.decode('utf-8') if isinstance(election.public_key, bytes) else str(election.public_key) # Public key is stored as base64-encoded bytes, try to decode it
pubkey_b64_str = election.public_key.decode('ascii') if isinstance(election.public_key, bytes) else str(election.public_key)
# Try to decode the base64 to verify it's valid
pubkey_bytes = base64.b64decode(pubkey_b64_str)
pubkey_str = pubkey_bytes.decode('utf-8')
# Check if it's valid (should be "p:g:h" format, not "pk_ongoing_X") # Check if it's valid (should be "p:g:h" format, not "pk_ongoing_X")
if not ':' in pubkey_str or pubkey_str.startswith('pk_') or pubkey_str.startswith('b\''): if not ':' in pubkey_str or pubkey_str.startswith('pk_') or pubkey_str.startswith('b\''):
pubkey_is_invalid = True pubkey_is_invalid = True
@ -165,8 +169,8 @@ async def init_election_keys(election_id: int, db: Session = Depends(get_db)):
if not election.public_key or pubkey_is_invalid: if not election.public_key or pubkey_is_invalid:
logger.info(f"Generating ElGamal public key for election {election_id}") logger.info(f"Generating ElGamal public key for election {election_id}")
elgamal = ElGamalEncryption(p=election.elgamal_p or 23, g=election.elgamal_g or 5) elgamal = ElGamalEncryption(p=election.elgamal_p or 23, g=election.elgamal_g or 5)
# Use the property that returns properly formatted bytes "p:g:h" # Store as base64-encoded bytes (public_key_bytes returns UTF-8 "p:g:h", then encode to base64)
election.public_key = elgamal.public_key_bytes election.public_key = base64.b64encode(elgamal.public_key_bytes)
db.commit() db.commit()
logger.info(f"✓ Generated public key for election {election_id}") logger.info(f"✓ Generated public key for election {election_id}")
else: else:
@ -179,7 +183,7 @@ async def init_election_keys(election_id: int, db: Session = Depends(get_db)):
"elgamal_p": election.elgamal_p, "elgamal_p": election.elgamal_p,
"elgamal_g": election.elgamal_g, "elgamal_g": election.elgamal_g,
"public_key_generated": True, "public_key_generated": True,
"public_key": base64.b64encode(election.public_key).decode() if election.public_key else None "public_key": election.public_key.decode('ascii') if isinstance(election.public_key, bytes) else election.public_key
} }
except HTTPException: except HTTPException:

View File

@ -510,7 +510,7 @@ async def setup_election(
"status": "initialized", "status": "initialized",
"election_id": election_id, "election_id": election_id,
"public_keys": { "public_keys": {
"elgamal_pubkey": election.public_key.decode('utf-8') if election.public_key else None "elgamal_pubkey": election.public_key.decode('ascii') if isinstance(election.public_key, bytes) else election.public_key
}, },
"blockchain_blocks": blockchain.get_block_count() "blockchain_blocks": blockchain.get_block_count()
} }
@ -547,7 +547,7 @@ async def get_public_keys(
) )
return { return {
"elgamal_pubkey": election.public_key.decode('utf-8') if election.public_key else None "elgamal_pubkey": election.public_key.decode('ascii') if isinstance(election.public_key, bytes) else election.public_key
} }