{-# LANGUAGE NoMonomorphismRestriction #-} {-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE StrictData #-} {-# OPTIONS_GHC -Wno-missing-signatures #-} module Hjugement.CLI.Voter where import Control.Applicative (Applicative(..), Alternative(..)) import Control.Monad (Monad(..), forM, forM_, join, unless, void, when) import Control.Monad.Trans.Class (MonadTrans(..)) import Control.Monad.Trans.Maybe (MaybeT(..)) import Control.Monad.Trans.Except (runExcept, runExceptT) import Control.Monad.Trans.State.Strict (runState, runStateT) import Data.Bits (setBit) import Data.Bool import Data.ByteString (ByteString) import Data.Either (Either(..), either) import Data.Eq (Eq(..)) import Data.Foldable (Foldable, foldMap, length, null, sum) import Data.Function (($), (.), id, flip) import Data.Functor ((<$>), (<$)) import Data.Int (Int) import Data.Maybe (Maybe(..), maybe, fromMaybe, fromJust) import Data.Monoid (Monoid(..)) import Data.Ord (Ord(..)) import Data.Proxy (Proxy(..)) import Data.Semigroup (Semigroup(..)) import Data.String (String) import Data.Text (Text) import Data.Traversable (sequence) import GHC.Natural (minusNatural, minusNaturalMaybe) import GHC.Prim (coerce) import Numeric.Natural (Natural) import Pipes ((>->)) import Prelude (logBase, ceiling, Num(..), (/), (^), fromIntegral, Double) import Symantic.CLI as CLI import System.IO (IO, FilePath) import Text.Show (Show(..)) import qualified Crypto.Hash as Crypto import qualified Data.Aeson as JSON import qualified Data.ByteArray as ByteArray import qualified Data.ByteString as BS import qualified Data.ByteString.Char8 as BS8 import qualified Data.ByteString.Lazy as BSL import qualified Data.ByteString.Lazy.Char8 as BSL8 import qualified Data.List as List import qualified Data.Text as Text import qualified Data.Text.Encoding as Text import qualified Data.Text.Lazy as TL import qualified Data.Text.Lazy.Builder as TLB import qualified Data.Text.Lazy.Builder.Int as TLB import qualified Data.Text.Lazy.Encoding as TL import qualified Data.Text.Lazy.IO as TL import qualified Data.Time as Time import qualified Lens.Family as Lens import qualified Lens.Family.State.Strict as Lens import qualified Pipes as Pip import qualified Pipes.Aeson as PipJSON (DecodingError(..)) import qualified Pipes.Aeson.Unchecked as PipJSON import qualified Pipes.ByteString as PipBS import qualified Pipes.Group as Pip import qualified Pipes.Prelude as Pip import qualified Pipes.Safe as Pip import qualified Pipes.Safe.Prelude as Pip import qualified Pipes.Text as PipText import qualified Pipes.Text.Encoding as PipText import qualified Pipes.Text.IO as PipText 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.Posix as Posix 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{..} o@VoterVote_Params{..} = runMaybeT $ do rawElec <- loadElection glob $ voterVote_url FP. "election.json" VP.reifyElection rawElec $ \(elec@VP.Election{..} :: VP.Election 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 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{..} o@VoterVerify_Params{..} = runMaybeT $ do rawElec <- loadElection glob $ voterVerify_url FP. "election.json" VP.reifyElection rawElec $ \(elec@VP.Election{..} :: VP.Election c) -> do outputInfo glob $ "verifying ballots" (fails :: Natural, (encTally :: VP.EncryptedTally 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 c <- loadJSON glob resultPath outputInfo glob $ "decrypting tally using trustees' decryption shares" trustees <- 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 ()