hjugement-cli-0.0.0.20190815: src/Hjugement/CLI/Administrator.hs
{-# LANGUAGE NoMonomorphismRestriction #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE StrictData #-}
{-# OPTIONS_GHC -Wno-missing-signatures #-}
module Hjugement.CLI.Administrator where
import Control.Applicative (Applicative(..))
import Control.Monad (Monad(..), forM_, unless)
import Control.Monad.Trans.Except (runExcept)
import Control.Monad.Trans.Maybe (MaybeT(..))
import Control.Monad.Trans.State.Strict (runState)
import Data.Bool
import Data.Either (Either(..))
import Data.Eq (Eq(..))
import Data.Function (($), id, flip)
import Data.Functor ((<$>))
import Data.Maybe (Maybe(..), fromMaybe)
import Data.Ord (Ord(..))
import Data.Proxy (Proxy(..))
import Data.Semigroup (Semigroup(..))
import Data.Text (Text)
import Symantic.CLI as CLI
import Text.Show (Show(..))
import qualified Data.Aeson as JSON
import qualified Data.ByteString.Lazy as BSL
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.FilePath as FP
import qualified System.Random as Rand
import qualified Voting.Protocol as VP
import Hjugement.CLI.Utils
-- * administrator
data Administrator_Params = Administrator_Params
{
} deriving (Show)
api_administrator =
"Commands for an administrator."
`helps`
command "administrator" $
api_administrator_election
<!> api_administrator_tally
<!> api_help False
run_administrator globParams =
run_administrator_election globParams
:!: run_administrator_tally globParams
:!: run_help api_administrator
-- ** election
data AdministratorElection_Params = AdministratorElection_Params
{ administratorElection_crypto :: VP.FFC
, administratorElection_name :: Text
, administratorElection_description :: Text
, administratorElection_uuid :: Maybe Text
, administratorElection_grades :: [Text]
, administratorElection_defaultGrade :: Maybe Text
} deriving (Show)
api_administrator_election =
"This command reads and checks the trustees' public keys in "<>fileRef"public_keys.jsons"<>".\
\ From which it computes the global election public key\
\ put it into an "<>fileRef"election.json"<>" file \
\ with the infos of the vote given here."
`helps`
command "election" $
rule "PARAMS"
(AdministratorElection_Params
<$> api_param_crypto
<*> api_param_name
<*> api_param_description
<*> api_option_uuid
<*> api_param_grades
<*> api_param_defaultGrade)
<?> api_quests
<.> response @(Maybe ())
where
api_param_name =
"Name of the election."
`help`
defaultTag "name" "" (var "STRING")
api_param_description =
"Description of the election."
`help`
defaultTag "description" "" (var "STRING")
api_option_uuid =
"UUID of the election."
`help`
optionalTag "uuid" $
var "UUID"
api_quests =
"Some questions."
`help`
many1 (var @Text "STRING")
api_param_grades =
"The grades to evaluate the choices, from the lowest to the highest."
`helps`
many1Tag (TagLong "grade") $
var @Text "STRING"
api_param_defaultGrade =
"The grade used when no grade is given by a voter.\n"<>
"Defaults to the lowest grade."
`helps`
optionalTag (TagLong "default-grade") $
var @Text "STRING"
run_administrator_election
glob@Global_Params{..}
AdministratorElection_Params{..}
quests =
VP.reify administratorElection_crypto $ \(_crypto::Proxy c) -> runMaybeT $ do
election_uuid <-
case administratorElection_uuid of
Nothing -> Pip.liftIO $ Rand.getStdRandom $ runState $ VP.randomUUID
Just u ->
case VP.readUUID u of
Left err -> outputError glob $ Doc.from (show err)
Right uuid -> return uuid
let trusteeKeysPath = global_dir FP.</> "public_keys.jsons"
trusteeKeys :: [VP.TrusteePublicKey c] <- runPipeWithError glob $
Pip.toListM' $ readJSON glob trusteeKeysPath
forM_ trusteeKeys $ \trusteeKey ->
case runExcept $ VP.verifyIndispensableTrusteePublicKey trusteeKey of
Left err -> outputError glob $ Doc.from (show err)
Right () -> return ()
let grades = List.nub administratorElection_grades
unless (List.length grades > 1) $
outputError glob $ "at least two distinct grades are needed"
unless (List.length grades == List.length administratorElection_grades) $
outputError glob $ "indistinct grades: " <>
Doc.from (Text.intercalate ", " $ List.nub $
administratorElection_grades List.\\ grades)
let defaultGrade =
fromMaybe (grades List.!!0)
administratorElection_defaultGrade
-- FIXME: put defaultGrade into election.json
let elec =
VP.Election
{ VP.election_name = administratorElection_name
, VP.election_description = administratorElection_description
, VP.election_crypto = VP.ElectionCrypto_FFC
{ electionCrypto_FFC_params = administratorElection_crypto
, electionCrypto_FFC_PublicKey = VP.combineIndispensableTrusteePublicKeys trusteeKeys
}
, VP.election_questions = (<$> quests) $ \quest -> VP.Question
{ question_text = quest
, question_choices = grades
, question_mini = 1
, question_maxi = 1
}
, VP.election_uuid
, VP.election_hash = VP.Base64SHA256 ""
}
saveJSON glob (global_dir FP.</> "election.json") elec
outputInfo glob $
"created election with "<>Doc.from (show election_uuid)<>
" and "<>Doc.from (show (VP.base64SHA256 (BSL.toStrict (JSON.encode elec))))
-- ** tally
api_administrator_tally =
"Tally an election using the decryption shares gathered from trustees\
\ in "<>fileRef "partial_decryptions.jsons"<>".\
\ The result is saved in "<>fileRef "result.json"<>".\n\
\ It contains the decryption shares,\
\ so "<>fileRef "partial_decryptions.jsons"<>" can be discarded afterwards."
`helps`
command "tally" $
response @(Maybe ())
run_administrator_tally
glob@Global_Params{..} = runMaybeT $ do
rawElec <- loadElection glob $ global_dir FP.</> "election.json"
VP.reifyElection rawElec $ \(_elec :: VP.Election c) -> do
keys <- runPipeWithError glob $ Pip.toListM' $
readJSON glob $ global_dir FP.</> "public_keys.jsons"
decs <- runPipeWithError glob $ Pip.toListM' $
readJSON glob $ global_dir FP.</> "partial_decryptions.jsons"
outputInfo glob $ "computing encrypted tally from ballots"
(encTally, numBallots) <- runPipeWithError glob $
Pip.fold'
(flip VP.insertEncryptedTally)
VP.emptyEncryptedTally id $
readJSON glob $ global_dir FP.</> "ballots.jsons"
outputInfo glob $ "decrypting tally using trustees' decryption shares"
case runExcept $ VP.proveTally
(encTally :: VP.EncryptedTally c, numBallots) decs
(VP.combineIndispensableDecryptionShares (VP.trustee_PublicKey <$> keys)) of
Left err -> outputError glob $ Doc.from (show err)
Right tally -> do
let resultPath = global_dir FP.</> "result.json"
saveJSON glob resultPath tally
outputInfo glob $ "tally generated in " <> Doc.from resultPath