packages feed

hjugement-cli-0.0.0.20190721: src/Hjugement/CLI/Trustee.hs

{-# LANGUAGE NoMonomorphismRestriction #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE StrictData #-}
{-# OPTIONS_GHC -Wno-missing-signatures #-}
module Hjugement.CLI.Trustee where

-- :main --dir /tmp/elec trust --crypto demo/groups/default.json gen
-- cat /tmp/elec/*.pubkey >/tmp/elec/public_keys.jsons

import Control.Applicative (Applicative(..))
import Control.Monad (Monad(..), forM, forM_, join, void)
import Control.Monad.Trans.Class (MonadTrans(..))
import Control.Monad.Trans.Maybe (MaybeT(..))
import Control.Monad.Trans.State.Strict (runState, runStateT)
import Data.Bits (setBit)
import Data.Bool
import Data.ByteString (ByteString)
import Data.Either (Either(..))
import Data.Eq (Eq(..))
import Data.Foldable (Foldable, foldMap, length, null)
import Data.Function (($), (.), id, flip)
import Data.Functor ((<$>))
import Data.Int (Int)
import Data.Maybe (Maybe(..), maybe, fromMaybe)
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 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 Text.Show (Show(..))
import System.IO (IO, FilePath)
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.List as List
import qualified Data.Text as T
import qualified Data.Text.Encoding as T
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.ByteString as PipBS
import qualified Pipes.Group as Pip
import qualified Pipes.Prelude as Pip
import qualified Pipes.Aeson as PipJSON (DecodingError(..))
import qualified Pipes.Aeson.Unchecked as PipJSON
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.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 Hjugement.CLI.Utils

-- * trustee
data Trustee_Params = Trustee_Params
 { trustee_crypto :: VP.FFC
 } deriving (Show)

api_trustee =
	"Commands for a trustee."
	`helps`
	command "trustee" $
		rule "TrusteeParams"
		 (Trustee_Params
			 <$> api_param_crypto
		 ) <?> (
			api_trustee_generate <!>
			api_trustee_decrypt
		 )
		<!> api_help False

run_trustee globParams =
	(\params ->
		run_trustee_generate globParams params :!:
		run_trustee_decrypt  globParams params
	 ) :!:
	run_help api_trustee

-- ** generate
api_trustee_generate =
 "Run by a trustee to generate a share of an election key.\
 \ Such a share consists of a private key and a public key with a certificate.\
 \ Generated files are stored in the current directory with\
 \ a name that starts with "<>fileRef "ID"<>",\
 \ where "<>fileRef "ID"<>" is a short fingerprint of the public key.\
 \ The private key is stored in "<>fileRef "ID.privkey"<>" and must be\
 \ secured by the trustee. The public key is stored in "<>fileRef "ID.pubkey"<>" and must\
 \ be sent to the election administrator."
	`helps`
	command "generate" $
		response @()
run_trustee_generate
 Global_Params{..}
 o@Trustee_Params{..} =
	VP.reify trustee_crypto $ \(crypto::Proxy c) -> do
		keys@(secKey, pubKey) <- Pip.liftIO $ Rand.getStdRandom $ runState $ do
			secKey <- VP.randomSecretKey @c
			pubKey <- VP.proveIndispensableTrusteePublicKey secKey
			return (secKey, pubKey)
		let pubIdent =
			T.unpack $ T.toUpper $ T.take 8 $
			VP.hexHash $ VP.bytesNat $
			VP.trustee_PublicKey pubKey
		runPipe $ do
			Pip.each [pubIdent] >-> pipeInfo (\ident ->
				"Generated trustee keypair "<>ident<>
				" in "<>(global_dir FP.</> ident)<>".{privkey,pubkey}"
			 ) >-> Pip.drain
			Pip.each [secKey] >-> writeJSON 0o400 (global_dir FP.</> pubIdent FP.<.>"privkey")
			Pip.each [pubKey] >-> writeJSON 0o444 (global_dir FP.</> pubIdent FP.<.>"pubkey")
		return ()

-- ** decrypt
data TrusteeDecrypt_Params = TrusteeDecrypt_Params
 {   trusteeDecrypt_privkey :: FilePath
 ,   trusteeDecrypt_url     :: FilePath
 } deriving (Show)

api_trustee_decrypt =
	"This command is run by each trustee to perform a partial decryption."
	`helps`
	command "decrypt" $
		rule "TrusteeDecryptParams"
		 (TrusteeDecrypt_Params
			 <$> api_param_privkey
			 <*> api_param_url)
		<?> response @(Maybe (VP.DecryptionShare ()))
	where
	api_param_privkey =
		"Read private key from file "<>ref"FILE"<>"."
		`helps`
		requiredTag "privkey" (var "FILE")
	api_param_url =
		"Download election files from "<>ref"URL"<>"."<>
		"\nDefaults to "<>fileRef "."<>"."
		`helps`
		defaultTag "url" "." (var "URL")

run_trustee_decrypt
 Global_Params{..}
 o@Trustee_Params{..}
 TrusteeDecrypt_Params{..} =
	VP.reify trustee_crypto $ \(crypto::Proxy c) -> runMaybeT $ do
		(secKey::VP.E c) <- loadJSON trusteeDecrypt_privkey
		let pubKey = VP.publicKey secKey
		let trusteeKeysPath = trusteeDecrypt_url FP.</> "public_keys.jsons"
		-- Check that the public key is amongst the public keys of the election
		keys <- runPipeWithError $
			Pip.toListM' $
				readJSON trusteeKeysPath
				 >-> Pip.filter ((pubKey ==) . VP.trustee_PublicKey)
		case () of
		 () | null keys -> outputError $
			"the public key associated with the given secret key "<>
			"is not within the list of public trustee keys of the election.\n"<>
			Doc.ul
			 [ "List of trustees' public keys: "<>Doc.from trusteeKeysPath
			 , "Trustee's public key: "<>Doc.from (show (VP.nat pubKey))
			 ]<>"\n"
		 () | List.length keys > 1 -> outputError $
			"the public key associated with the given secret key "<>
			"appears more than one time in the list of public trustee keys of the election.\n"<>
			Doc.ul
			 [ "List of trustees' public keys: "<>Doc.from trusteeKeysPath
			 , "Trustee's public key: "<>Doc.from (show (VP.nat pubKey))
			 ]<>"\n"
		 () -> do
			-- Tally the encrypted ballots
			-- FIXME: actually support fetching through an URL
			let ballotsPath = trusteeDecrypt_url FP.</> "ballots.jsons"
			(encTally, numBallots) <- runPipeWithError $
				Pip.fold'
				 (flip VP.insertEncryptedTally)
				 VP.emptyEncryptedTally id $
					readJSON ballotsPath
			decShare <- Pip.liftIO $
				Rand.getStdRandom $ runState $
					VP.proveDecryptionShare encTally secKey
			return (coerce decShare :: VP.DecryptionShare ())