1 {-# LANGUAGE NoMonomorphismRestriction #-}
2 {-# LANGUAGE OverloadedStrings #-}
3 {-# LANGUAGE StrictData #-}
4 {-# LANGUAGE TypeApplications #-}
5 {-# OPTIONS_GHC -Wno-missing-signatures #-}
6 module Hjugement.CLI.Voter where
8 import Control.Applicative (Applicative(..), Alternative(..))
9 import Control.Monad (Monad(..), join, unless, void, when)
10 import Control.Monad.Trans.Maybe (MaybeT(..))
11 import Control.Monad.Trans.Except (runExcept)
12 import Control.Monad.Trans.State.Strict (runStateT)
14 import Data.Either (Either(..))
15 import Data.Eq (Eq(..))
16 import Data.Foldable (sum)
17 import Data.Function (($))
18 import Data.Functor ((<$>))
19 import Data.Maybe (Maybe(..))
20 import Data.Ord (Ord(..))
21 import Data.Semigroup (Semigroup(..))
22 import Data.String (String)
23 import Data.Text (Text)
24 import Numeric.Natural (Natural)
25 import Prelude (Num(..))
26 import Symantic.CLI as CLI
27 import System.IO (FilePath)
28 import Text.Show (Show(..))
29 import qualified Data.Aeson as JSON
30 import qualified Data.ByteString.Lazy.Char8 as BSL8
31 import qualified Data.List as List
32 import qualified Data.Text as Text
33 import qualified Pipes as Pip
34 import qualified Pipes.Prelude as Pip
35 import qualified Symantic.Document as Doc
36 import qualified System.Directory as IO
37 import qualified System.FilePath as FP
38 import qualified System.IO as IO
39 import qualified System.Random as Rand
40 import qualified Voting.Protocol as VP
41 import qualified Voting.Protocol.Utils as VP
43 import Hjugement.CLI.Utils
46 data Voter_Params = Voter_Params
51 "Commands for a voter."
57 run_voter globParams =
58 run_voter_vote globParams
59 :!: run_voter_verify globParams
60 :!: run_help api_voter
63 data VoterVote_Params = VoterVote_Params
64 { voterVote_privcred :: VP.Credential
65 , voterVote_url :: FilePath
66 , voterVote_grades :: [Text]
70 "Cast a vote on an election."
75 <$> api_param_privcred
78 <?> response @(Maybe ())
81 "Voter's private credential."
83 requiredTag "privcred" (var "CREDENTIAL")
85 "The grades to evaluate the choices, from the lowest to the highest."
87 many1Tag (TagLong "grade") $
91 glob@Global_Params{..}
92 VoterVote_Params{..} = runMaybeT $ do
93 rawElec <- loadElection glob $ voterVote_url FP.</> "election.json"
94 VP.reifyElection rawElec $ \(elec@VP.Election{..} :: VP.Election c) -> do
95 outputInfo glob $ "Voted"<>Doc.from (show voterVote_grades)
96 votes <- VP.isoZipWithM
97 (outputError glob $ "Mismatching number of cast grades ("<>
98 Doc.from (List.length voterVote_grades)<>
100 Doc.from (List.length election_questions)<>
102 (\VP.Question{..} grade -> do
103 let bools = (grade ==) <$> question_choices
104 let boolSum = sum $ (<$> bools) $ \b -> if b then 1 else 0 :: Natural
105 unless (boolSum == 1) $
107 "The election does not allow to cast a grade named: "<>Doc.from grade<>"\n"<>
108 "Allowed grades are: "<>Doc.from (Text.intercalate ", " question_choices)
112 outputInfo glob $ Doc.from (show votes)
113 let (secKey :: VP.SecretKey c) =
114 VP.credentialSecretKey election_uuid voterVote_privcred
115 ballot <- join $ Pip.liftIO $
116 Rand.getStdRandom $ \gen ->
117 case runExcept $ (`runStateT` gen) $
118 VP.encryptBallot elec (Just secKey) votes of
119 Left err -> (outputError glob $ Doc.from (show err), gen)
120 Right (ballot, gen') -> (return ballot, gen')
121 Pip.liftIO $ BSL8.putStrLn $ JSON.encode ballot
124 data VoterVerify_Params = VoterVerify_Params
125 { voterVerify_url :: FilePath
129 "Cast a vote on an election."
135 <?> response @(Maybe ())
138 glob@Global_Params{..}
139 VoterVerify_Params{..} = runMaybeT $ do
140 rawElec <- loadElection glob $ voterVerify_url FP.</> "election.json"
141 VP.reifyElection rawElec $ \(elec@VP.Election{..} :: VP.Election c) -> do
142 outputInfo glob $ "verifying ballots"
143 (fails :: Natural, (encTally :: VP.EncryptedTally c, _numBallots)) <- runPipeWithError glob $
145 (\(fails, acc@(_, numBallots)) ballot@VP.Ballot{..} -> do
146 let ballotNum = numBallots + fails
148 { global_stderr_prepend_carriage = True
149 , global_stderr_append_newline = False
151 "checking ballot #"<>Doc.from ballotNum
152 let globError = glob{global_stderr_prepend_newline = Verbosity_Debug <= global_verbosity}
153 case ballot_signature of
155 void $ runMaybeT $ outputError globError $
156 "ballot #"<>Doc.from ballotNum<>" has no signature"
157 return (fails+1, acc)
159 if VP.verifyBallot elec ballot
160 then return (fails, VP.insertEncryptedTally ballot acc)
162 void $ runMaybeT $ outputError globError $
163 "ballot #"<>Doc.from ballotNum<>" has an invalid signature"
164 return (fails+1, acc)
166 (return (0, VP.emptyEncryptedTally))
168 readJSON glob $ voterVerify_url FP.</> "ballots.jsons"
169 when (Verbosity_Debug <= global_verbosity) $
170 Pip.liftIO $ output $ OnHandle IO.stderr (Doc.newline::String)
171 when (0 < fails) empty
172 let resultPath = voterVerify_url FP.</> "result.json"
173 hasResult <- Pip.liftIO $ IO.doesPathExist resultPath
176 outputWarning glob "no tally to check"
178 tally :: VP.Tally c <- loadJSON glob resultPath
179 outputInfo glob $ "decrypting tally using trustees' decryption shares"
180 trustees <- runPipeWithError glob $ Pip.toListM' $
181 readJSON glob $ voterVerify_url FP.</> "public_keys.jsons"
182 let trustPubKeys = VP.trustee_PublicKey <$> trustees
183 decs <- runPipeWithError glob $ Pip.toListM' $
184 readJSON glob $ voterVerify_url FP.</> "partial_decryptions.jsons"
185 outputInfo glob $ "verifying tally"
187 VP.verifyDecryptionShareByTrustee encTally trustPubKeys decs
188 VP.verifyTally tally (VP.combineIndispensableDecryptionShares trustPubKeys)
190 Left err -> outputError glob $ Doc.from (show err)
191 Right () -> return ()