packages feed

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