1- from typing import Any , Optional
2- from electionguard .ballot import CiphertextBallot , PlaintextBallot
1+ from typing import Any , Dict , List , Optional
2+ from electionguard .ballot import (
3+ CiphertextAcceptedBallot ,
4+ CiphertextBallot ,
5+ PlaintextBallot ,
6+ )
37from electionguard .ballot_box import accept_ballot , BallotBoxState
8+ from electionguard .decrypt_with_shares import decrypt_ballot
9+ from electionguard .decryption_share import BallotDecryptionShare
410from electionguard .ballot_store import BallotStore
511from electionguard .election import (
612 CiphertextElectionContext ,
1016from electionguard .encrypt import encrypt_ballot
1117from electionguard .group import ElementModQ
1218from electionguard .serializable import read_json_object , write_json_object
19+ from electionguard .types import BALLOT_ID , GUARDIAN_ID
1320from electionguard .utils import get_optional
1421from fastapi import APIRouter , Body , HTTPException
1522
16- from ..models import AcceptBallotRequest , EncryptBallotsRequest , EncryptBallotsResponse
17- from ..tags import CAST_AND_SPOIL , ENCRYPT_BALLOTS
23+ from ..models import (
24+ AcceptBallotRequest ,
25+ DecryptBallotsRequest ,
26+ EncryptBallotsRequest ,
27+ EncryptBallotsResponse ,
28+ )
29+ from ..tags import CAST_AND_SPOIL , ENCRYPT_BALLOTS , TALLY
1830
1931router = APIRouter ()
2032
@@ -33,6 +45,34 @@ def cast_ballot(request: AcceptBallotRequest = Body(...)) -> Any:
3345 return casted_ballot .to_json_object ()
3446
3547
48+ @router .post ("/decrypt" , tags = [TALLY ])
49+ def decrypt_ballots (request : DecryptBallotsRequest = Body (...)) -> Any :
50+ ballots = [
51+ CiphertextAcceptedBallot .from_json_object (ballot )
52+ for ballot in request .encrypted_ballots
53+ ]
54+ context : CiphertextElectionContext = CiphertextElectionContext .from_json_object (
55+ request .context
56+ )
57+
58+ all_shares : List [BallotDecryptionShare ] = [
59+ read_json_object (share , BallotDecryptionShare )
60+ for shares in request .shares .values ()
61+ for share in shares
62+ ]
63+ shares_by_ballot = index_shares_by_ballot (all_shares )
64+
65+ extended_base_hash = context .crypto_extended_base_hash
66+ decrypted_ballots = {
67+ ballot .object_id : decrypt_ballot (
68+ ballot , shares_by_ballot [ballot .object_id ], extended_base_hash
69+ )
70+ for ballot in ballots
71+ }
72+
73+ return write_json_object (decrypted_ballots )
74+
75+
3676@router .post ("/spoil" , tags = [CAST_AND_SPOIL ])
3777def spoil_ballot (request : AcceptBallotRequest = Body (...)) -> Any :
3878 """
@@ -96,3 +136,18 @@ def handle_ballot(request: AcceptBallotRequest, state: BallotBoxState) -> Any:
96136 )
97137
98138 return accepted_ballot
139+
140+
141+ def index_shares_by_ballot (
142+ shares : List [BallotDecryptionShare ],
143+ ) -> Dict [BALLOT_ID , Dict [GUARDIAN_ID , BallotDecryptionShare ]]:
144+ """
145+ Construct a lookup by ballot ID containing the dictionary of shares needed
146+ to decrypt that ballot.
147+ """
148+ shares_by_ballot : Dict [str , Dict [str , BallotDecryptionShare ]] = {}
149+ for share in shares :
150+ ballot_shares = shares_by_ballot .setdefault (share .ballot_id , {})
151+ ballot_shares [share .guardian_id ] = share
152+
153+ return shares_by_ballot
0 commit comments