Compare commits

...

2 Commits

Author SHA1 Message Date
Alexis Bruneteau
78f0140342 fix: Handle both plain and base64-encoded public keys in API responses
## Issue
Even after storing keys as base64, the API was returning plain "p:g:h" format
for existing elections that had keys stored as plain UTF-8 bytes, causing:
- Client receives: "23:5:13" (plain text)
- Client tries to decode as base64 (btoa call)
- Results in: "Invalid base64: 23:5:13... - String contains an invalid character"

## Root Cause
1. Old elections have public_key stored as plain UTF-8: b'23:5:13'
2. New elections store as base64: b'MjM6NToxMw=='
3. Both were decoded to string before return, exposing wrong format
4. Also fixed ElGamal class name typo: ElGamal() → ElGamalEncryption()

## Fix
1. Detect public key format before returning:
   - If plain "p:g:h" format (contains ':'), encode to base64
   - If already base64 (starts with 'MjM6'), return as-is
2. Always return base64-encoded string to client
3. Updated both /setup and /public-keys endpoints in votes.py
4. Updated /init-keys endpoint in admin.py
5. Fixed class name in setup_election function

## Files Changed
- backend/routes/votes.py: Lines 502, 509-518, 560-569
- backend/routes/admin.py: Lines 179-197

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-11 19:36:39 +01:00
Alexis Bruneteau
adfec105d8 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>
2025-11-11 19:33:22 +01:00
2 changed files with 44 additions and 7 deletions

View File

@ -155,7 +155,11 @@ async def init_election_keys(election_id: int, db: Session = Depends(get_db)):
pubkey_is_invalid = False
if election.public_key:
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")
if not ':' in pubkey_str or pubkey_str.startswith('pk_') or pubkey_str.startswith('b\''):
pubkey_is_invalid = True
@ -165,13 +169,24 @@ async def init_election_keys(election_id: int, db: Session = Depends(get_db)):
if not election.public_key or pubkey_is_invalid:
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)
# Use the property that returns properly formatted bytes "p:g:h"
election.public_key = elgamal.public_key_bytes
# Store as base64-encoded bytes (public_key_bytes returns UTF-8 "p:g:h", then encode to base64)
election.public_key = base64.b64encode(elgamal.public_key_bytes)
db.commit()
logger.info(f"✓ Generated public key for election {election_id}")
else:
logger.info(f"Election {election_id} already has valid public key")
# Ensure public key is base64-encoded for client
pubkey_to_return = election.public_key
if isinstance(pubkey_to_return, bytes):
pubkey_str = pubkey_to_return.decode('utf-8')
# If it's plain "p:g:h" format, encode it to base64
if ':' in pubkey_str and not pubkey_str.startswith('MjM6'): # Not already base64
pubkey_to_return = base64.b64encode(pubkey_str.encode('utf-8')).decode('ascii')
else:
# Already base64, just decode to string
pubkey_to_return = pubkey_str
return {
"status": "success",
"election_id": election_id,
@ -179,7 +194,7 @@ async def init_election_keys(election_id: int, db: Session = Depends(get_db)):
"elgamal_p": election.elgamal_p,
"elgamal_g": election.elgamal_g,
"public_key_generated": True,
"public_key": base64.b64encode(election.public_key).decode() if election.public_key else None
"public_key": pubkey_to_return
}
except HTTPException:

View File

@ -499,18 +499,29 @@ async def setup_election(
# Générer les clés ElGamal si nécessaire
if not election.public_key:
elgamal = ElGamal()
elgamal = ElGamalEncryption()
# 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()
# Ensure public key is base64-encoded for client
pubkey_to_return = election.public_key
if isinstance(pubkey_to_return, bytes):
pubkey_str = pubkey_to_return.decode('utf-8')
# If it's plain "p:g:h" format, encode it to base64
if ':' in pubkey_str and not pubkey_str.startswith('MjM6'): # Not already base64
pubkey_to_return = base64.b64encode(pubkey_str.encode('utf-8')).decode('ascii')
else:
# Already base64, just decode to string
pubkey_to_return = pubkey_str
return {
"status": "initialized",
"election_id": election_id,
"public_keys": {
"elgamal_pubkey": election.public_key.decode('utf-8') if election.public_key else None
"elgamal_pubkey": pubkey_to_return
},
"blockchain_blocks": blockchain.get_block_count()
}
@ -546,8 +557,19 @@ async def get_public_keys(
detail="Election keys not initialized. Call /setup first."
)
# Ensure public key is base64-encoded for client
pubkey_to_return = election.public_key
if isinstance(pubkey_to_return, bytes):
pubkey_str = pubkey_to_return.decode('utf-8')
# If it's plain "p:g:h" format, encode it to base64
if ':' in pubkey_str and not pubkey_str.startswith('MjM6'): # Not already base64
pubkey_to_return = base64.b64encode(pubkey_str.encode('utf-8')).decode('ascii')
else:
# Already base64, just decode to string
pubkey_to_return = pubkey_str
return {
"elgamal_pubkey": election.public_key.decode('utf-8') if election.public_key else None
"elgamal_pubkey": pubkey_to_return
}