hjugement-cli-0.0.0.20191104: src/Hjugement/CLI/Voter.hs
{-# LANGUAGE NoMonomorphismRestriction #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE StrictData #-}
{-# LANGUAGE TypeApplications #-}
{-# OPTIONS_GHC -Wno-missing-signatures #-}
module Hjugement.CLI.Voter where
import Control.Applicative (Applicative(..), Alternative(..))
import Control.Monad (Monad(..), join, unless, void, when)
import Control.Monad.Trans.Maybe (MaybeT(..))
import Control.Monad.Trans.Except (runExcept)
import Control.Monad.Trans.State.Strict (runStateT)
import Data.Bool
import Data.Either (Either(..))
import Data.Eq (Eq(..))
import Data.Foldable (sum)
import Data.Function (($))
import Data.Functor ((<$>))
import Data.Maybe (Maybe(..))
import Data.Ord (Ord(..))
import Data.Semigroup (Semigroup(..))
import Data.String (String)
import Data.Text (Text)
import Numeric.Natural (Natural)
import Prelude (Num(..))
import Symantic.CLI as CLI
import System.IO (FilePath)
import Text.Show (Show(..))
import qualified Data.Aeson as JSON
import qualified Data.ByteString.Lazy.Char8 as BSL8
import qualified Data.List as List
import qualified Data.Text as Text
import qualified Pipes as Pip
import qualified Pipes.Prelude as Pip
import qualified Symantic.Document as Doc
import qualified System.Directory as IO
import qualified System.FilePath as FP
import qualified System.IO as IO
import qualified System.Random as Rand
import qualified Voting.Protocol as VP
import qualified Voting.Protocol.Utils as VP
import Hjugement.CLI.Utils
-- * administrator
data Voter_Params = Voter_Params
{
} deriving (Show)
api_voter =
"Commands for a voter."
`helps`
command "voter" $
api_voter_vote
<!> api_voter_verify
<!> api_help False
run_voter globParams =
run_voter_vote globParams
:!: run_voter_verify globParams
:!: run_help api_voter
-- ** vote
data VoterVote_Params = VoterVote_Params
{ voterVote_privcred :: VP.Credential
, voterVote_url :: FilePath
, voterVote_grades :: [Text]
} deriving (Show)
api_voter_vote =
"Cast a vote on an election."
`helps`
command "vote" $
rule "PARAMS"
(VoterVote_Params
<$> api_param_privcred
<*> api_param_url
<*> api_param_grades)
<?> response @(Maybe ())
where
api_param_privcred =
"Voter's private credential."
`helps`
requiredTag "privcred" (var "CREDENTIAL")
api_param_grades =
"The grades to evaluate the choices, from the lowest to the highest."
`helps`
many1Tag (TagLong "grade") $
var @Text "STRING"
run_voter_vote
glob@Global_Params{..}
VoterVote_Params{..} = runMaybeT $ do
loadElection glob (voterVote_url FP.</> "election.json") $
\(elec@VP.Election{..} :: VP.Election VP.FFC v c) -> do
outputInfo glob $ "Voted"<>Doc.from (show voterVote_grades)
votes <- VP.isoZipWithM
(outputError glob $ "Mismatching number of cast grades ("<>
Doc.from (List.length voterVote_grades)<>
") and choices ("<>
Doc.from (List.length election_questions)<>
")")
(\VP.Question{..} grade -> do
let bools = (grade ==) <$> question_choices
let boolSum = sum $ (<$> bools) $ \b -> if b then 1 else 0 :: Natural
unless (boolSum == 1) $
outputError glob $
"The election does not allow to cast a grade named: "<>Doc.from grade<>"\n"<>
"Allowed grades are: "<>Doc.from (Text.intercalate ", " question_choices)
return bools)
election_questions
voterVote_grades
outputInfo glob $ Doc.from (show votes)
let (secKey :: VP.SecretKey VP.FFC c) =
VP.credentialSecretKey election_uuid voterVote_privcred
ballot <- join $ Pip.liftIO $
Rand.getStdRandom $ \gen ->
case runExcept $ (`runStateT` gen) $
VP.encryptBallot elec (Just secKey) votes of
Left err -> (outputError glob $ Doc.from (show err), gen)
Right (ballot, gen') -> (return ballot, gen')
Pip.liftIO $ BSL8.putStrLn $ JSON.encode ballot
-- ** verify
data VoterVerify_Params = VoterVerify_Params
{ voterVerify_url :: FilePath
} deriving (Show)
api_voter_verify =
"Cast a vote on an election."
`helps`
command "verify" $
rule "PARAMS"
(VoterVerify_Params
<$> api_param_url)
<?> response @(Maybe ())
run_voter_verify
glob@Global_Params{..}
VoterVerify_Params{..} = runMaybeT $ do
loadElection glob (voterVerify_url FP.</> "election.json") $
\(elec@VP.Election{..} :: VP.Election VP.FFC v c) -> do
outputInfo glob $ "verifying ballots"
(fails :: Natural, (encTally :: VP.EncryptedTally VP.FFC v c, _numBallots))
<- runPipeWithError glob $
Pip.foldM'
(\(fails, acc@(_, numBallots)) ballot@VP.Ballot{..} -> do
let ballotNum = numBallots + fails
outputDebug glob
{ global_stderr_prepend_carriage = True
, global_stderr_append_newline = False
} $
"checking ballot #"<>Doc.from ballotNum
let globError = glob{global_stderr_prepend_newline = Verbosity_Debug <= global_verbosity}
case ballot_signature of
Nothing -> do
void $ runMaybeT $ outputError globError $
"ballot #"<>Doc.from ballotNum<>" has no signature"
return (fails+1, acc)
Just{} ->
if VP.verifyBallot elec ballot
then return (fails, VP.insertEncryptedTally ballot acc)
else do
void $ runMaybeT $ outputError globError $
"ballot #"<>Doc.from ballotNum<>" has an invalid signature"
return (fails+1, acc)
)
(return (0, VP.emptyEncryptedTally))
return $
readJSON glob $ voterVerify_url FP.</> "ballots.jsons"
when (Verbosity_Debug <= global_verbosity) $
Pip.liftIO $ output $ OnHandle IO.stderr (Doc.newline::String)
when (0 < fails) empty
let resultPath = voterVerify_url FP.</> "result.json"
hasResult <- Pip.liftIO $ IO.doesPathExist resultPath
if not hasResult
then do
outputWarning glob "no tally to check"
else do
tally :: VP.Tally VP.FFC v c <- loadJSON glob resultPath
outputInfo glob $ "decrypting tally using trustees' decryption shares"
trustees :: [VP.TrusteePublicKey VP.FFC v c]
<- runPipeWithError glob $ Pip.toListM' $
readJSON glob $ voterVerify_url FP.</> "public_keys.jsons"
let trustPubKeys = VP.trustee_PublicKey <$> trustees
decs <- runPipeWithError glob $ Pip.toListM' $
readJSON glob $ voterVerify_url FP.</> "partial_decryptions.jsons"
outputInfo glob $ "verifying tally"
case runExcept $ do
VP.verifyDecryptionShareByTrustee encTally trustPubKeys decs
VP.verifyTally tally (VP.combineIndispensableDecryptionShares trustPubKeys)
of
Left err -> outputError glob $ Doc.from (show err)
Right () -> return ()