]> Git — Sourcephile - majurity.git/blob - hjugement-protocol/Protocol/Election.hs
protocol: remove ErrorProve
[majurity.git] / hjugement-protocol / Protocol / Election.hs
1 {-# LANGUAGE DataKinds #-}
2 {-# LANGUAGE GADTs #-}
3 {-# LANGUAGE OverloadedStrings #-}
4 module Protocol.Election where
5
6 import Control.Monad (Monad(..), join, mapM, zipWithM)
7 import Control.Monad.Morph (MFunctor(..))
8 import Control.Monad.Trans.Class (MonadTrans(..))
9 import Data.Bool
10 import Data.Either (either)
11 import Data.Eq (Eq(..))
12 import Data.Foldable (Foldable, foldMap, and)
13 import Data.Function (($), id, const)
14 import Data.Functor (Functor, (<$>))
15 import Data.Functor.Identity (Identity(..))
16 import Data.Maybe (Maybe(..), fromMaybe)
17 import Data.Ord (Ord(..))
18 import Data.Semigroup (Semigroup(..))
19 import Data.Text (Text)
20 import Data.Traversable (Traversable(..))
21 import Data.Tuple (fst, snd, uncurry)
22 import GHC.Natural (minusNaturalMaybe)
23 import Numeric.Natural (Natural)
24 import Prelude (fromIntegral)
25 import Text.Show (Show(..))
26 import qualified Control.Monad.Trans.Except as Exn
27 import qualified Control.Monad.Trans.State.Strict as S
28 import qualified Data.ByteString as BS
29 import qualified Data.List as List
30
31 import Protocol.Arithmetic
32 import Protocol.Credential
33
34 -- * Type 'Encryption'
35 -- | ElGamal-like encryption.
36 -- Its security relies on the /Discrete Logarithm problem/.
37 --
38 -- Because ('groupGen' '^'encNonce '^'secKey '==' 'groupGen' '^'secKey '^'encNonce),
39 -- knowing @secKey@, one can divide 'encryption_vault' by @('encryption_nonce' '^'secKey)@
40 -- to decipher @('groupGen' '^'clear)@, then the @clear@ text must be small to be decryptable,
41 -- because it is encrypted as a power of 'groupGen' (hence the "-like" in "ElGamal-like")
42 -- to enable the additive homomorphism.
43 --
44 -- NOTE: Since @('encryption_vault' '*' 'encryption_nonce' '==' 'encryption_nonce' '^' (secKey '+' clear))@,
45 -- then: @(logBase 'encryption_nonce' ('encryption_vault' '*' 'encryption_nonce') '==' secKey '+' clear)@.
46 data Encryption q = Encryption
47 { encryption_nonce :: G q
48 -- ^ Public part of the randomness 'encNonce' used to 'encrypt' the 'clear' text,
49 -- equal to @('groupGen' '^'encNonce)@
50 , encryption_vault :: G q
51 -- ^ Encrypted 'clear' text, equal to @('pubKey' '^'r '*' 'groupGen' '^'clear)@
52 } deriving (Eq,Show)
53
54 -- | Additive homomorphism.
55 -- Using the fact that: @'groupGen' '^'x '*' 'groupGen' '^'y '==' 'groupGen' '^'(x'+'y)@.
56 instance SubGroup q => Additive (Encryption q) where
57 zero = Encryption one one
58 x+y = Encryption
59 (encryption_nonce x * encryption_nonce y)
60 (encryption_vault x * encryption_vault y)
61
62 -- *** Type 'EncryptionNonce'
63 type EncryptionNonce = E
64
65 -- | @('encrypt' pubKey clear)@ returns an ElGamal-like 'Encryption'.
66 --
67 -- WARNING: the secret encryption nonce (@encNonce@)
68 -- is returned alongside the 'Encryption'
69 -- in order to 'prove' the validity of the encrypted 'clear' text in 'proveEncryption',
70 -- but this secret @encNonce@ MUST be forgotten after that,
71 -- as it may be used to decipher the 'Encryption'
72 -- without the secret key associated with 'pubKey'.
73 encrypt ::
74 Monad m => RandomGen r => SubGroup q =>
75 PublicKey q -> E q ->
76 S.StateT r m (EncryptionNonce q, Encryption q)
77 encrypt pubKey clear = do
78 encNonce <- random
79 -- NOTE: preserve the 'encNonce' for 'prove' in 'proveEncryption'.
80 return $ (encNonce,)
81 Encryption
82 { encryption_nonce = groupGen^encNonce
83 , encryption_vault = pubKey ^encNonce * groupGen^clear
84 }
85
86 -- * Type 'Proof'
87 -- | 'Proof' of knowledge of a discrete logarithm:
88 -- @(secret == logBase base (base^secret))@.
89 data Proof q = Proof
90 { proof_challenge :: Challenge q
91 -- ^ 'Challenge' sent by the verifier to the prover
92 -- to ensure that the prover really has knowledge
93 -- of the secret and is not replaying.
94 -- Actually, 'proof_challenge' is not sent to the prover,
95 -- but derived from the prover's 'Commitment's and statements
96 -- with a collision resistant 'hash'.
97 -- Hence the prover cannot chose the 'proof_challenge' to his/her liking.
98 , proof_response :: E q
99 -- ^ A discrete logarithm sent by the prover to the verifier,
100 -- as a response to 'proof_challenge'.
101 --
102 -- If the verifier observes that @('proof_challenge' '==' 'hash' statement [commitment])@
103 -- where:
104 --
105 -- * @statement@ is a serialization of a tag, 'base' and 'basePowSec',
106 -- * @(commitment '==' 'commit' proof base basePowSec '=='
107 -- base '^' 'proof_response' '*' basePowSec '^' 'proof_challenge')@,
108 -- * and @(basePowSec '==' base'^'sec)@,
109 --
110 -- then, with overwhelming probability due to the 'hash' function:
111 -- @(commitment '==' base'^'nonce)@.
112 -- Therefore by expanding 'commitment':
113 -- @('proof_response' '==' logBase base (base'^'nonce) '-' logBase basePowSec (basePowSec '^' 'proof_challenge'))@,
114 -- which means that the prover must have known 'nonce' and 'sec'
115 -- to compute 'proof_response' efficiently with:
116 -- @('proof_response' '==' nonce '-' sec '*' 'proof_challenge')@,
117 --
118 -- The 'nonce' is introduced to ensure each 'prove' does not reveal
119 -- any information regarding the prover's secret 'sec',
120 -- by being randomly chosen by the prover.
121 } deriving (Eq,Show)
122
123 -- ** Type 'ZKP'
124 -- | Zero-knowledge proof
125 --
126 -- DOC: Mihir Bellare and Phillip Rogaway. Random oracles are practical:
127 -- A paradigm for designing efficient protocols. In ACM-CCS’93, 1993.
128 --
129 -- DOC: Pierrick Gaudry. <https://hal.inria.fr/hal-01576379 Some ZK security proofs for Belenios>, 2017.
130 newtype ZKP = ZKP BS.ByteString
131
132 -- ** Type 'Challenge'
133 type Challenge = E
134
135 -- ** Type 'Oracle'
136 -- An 'Oracle' returns the 'Challenge' of the 'Commitment's
137 -- by 'hash'ing them (eventually with other 'Commitment's).
138 --
139 -- Used in 'prove' it enables a Fiat-Shamir transformation
140 -- of an /interactive zero-knowledge/ (IZK) proof
141 -- into a /non-interactive zero-knowledge/ (NIZK) proof.
142 -- That is to say that the verifier does not have
143 -- to send a 'Challenge' to the prover.
144 -- Indeed, the prover now handles the 'Challenge'
145 -- which becomes a (collision resistant) 'hash'
146 -- of the prover's commitments (and statements to be a stronger proof).
147 type Oracle list q = list (Commitment q) -> Challenge q
148
149 -- | @('prove' sec commitBases oracle)@
150 -- returns a 'Proof' that @sec@ is known.
151 --
152 -- The 'Oracle' is given the 'commitBases'
153 -- raised to the power of the secret nonce of the 'Proof',
154 -- as those are the 'commitBases' that the verifier will obtain
155 -- when composing the 'proof_challenge' and 'proof_response' together
156 -- (in 'commit').
157 --
158 -- NOTE: 'sec' is @secKey@ in 'signature_proof' or @encNonce@ in 'proveEncryption'.
159 --
160 -- WARNING: for 'prove' to be a so-called /strong Fiat-Shamir transformation/ (not a weak):
161 -- the statement must be included in the 'hash' (not only the commitments).
162 --
163 -- NOTE: a 'random' @nonce@ is used to ensure each 'prove'
164 -- does not reveal any information regarding the secret 'sec'.
165 prove ::
166 Monad m => RandomGen r => SubGroup q => Functor list =>
167 E q -> list (Commitment q) -> Oracle list q -> S.StateT r m (Proof q)
168 prove sec commitBases oracle = do
169 nonce <- random
170 let proof_challenge = oracle $ (^ nonce) <$> commitBases
171 return Proof
172 { proof_challenge
173 , proof_response = nonce - sec*proof_challenge
174 }
175
176 -- ** Type 'Commitment'
177 type Commitment = G
178
179 -- | @('commit' proof base basePowSec)@ returns a 'Commitment'
180 -- from the given 'Proof' with the knowledge of the verifier.
181 commit :: SubGroup q => Proof q -> G q -> G q -> Commitment q
182 commit Proof{..} base basePowSec =
183 base^proof_response *
184 basePowSec^proof_challenge
185 -- NOTE: Contrary to some textbook presentations,
186 -- @('*')@ is used instead of @('/')@ to avoid the performance cost
187 -- of a modular exponentiation @('^' ('groupOrder' '-' 'one'))@,
188 -- this is compensated by using @('-')@ instead of @('+')@ in 'prove'.
189 {-# INLINE commit #-}
190
191 -- * Type 'Disjunction'
192 -- | A 'Disjunction' is an 'inv'ersed @('groupGen' '^'opinion)@
193 -- it's used in 'proveEncryption' to generate a 'Proof'
194 -- that an 'encryption_vault' contains a given @('groupGen' '^'opinion)@,
195 type Disjunction = G
196
197 booleanDisjunctions :: SubGroup q => [Disjunction q]
198 booleanDisjunctions = List.take 2 groupGenInverses
199
200 intervalDisjunctions :: SubGroup q => Opinion q -> Opinion q -> [Disjunction q]
201 intervalDisjunctions mini maxi =
202 List.genericTake (fromMaybe 0 $ (nat maxi + 1)`minusNaturalMaybe`nat mini) $
203 List.genericDrop (nat mini) $
204 groupGenInverses
205
206 -- ** Type 'Opinion'
207 -- | Index of a 'Disjunction' within a list of them.
208 -- It is encrypted as an 'E'xponent by 'encrypt'.
209 type Opinion = E
210
211 -- ** Type 'DisjProof'
212 -- | A list of 'Proof's to prove that the 'Opinion' within an 'Encryption'
213 -- is indexing a 'Disjunction' within a list of them,
214 -- without revealing which 'Opinion' it is.
215 newtype DisjProof q = DisjProof [Proof q]
216 deriving (Eq,Show)
217
218 -- | @('proveEncryption' elecPubKey voterZKP (prevDisjs,nextDisjs) (encNonce,enc))@
219 -- returns a 'DisjProof' that 'enc' 'encrypt's
220 -- the 'Disjunction's between 'prevDisjs' and 'nextDisjs'.
221 --
222 -- A /NIZK Disjunctive Chaum Pedersen Logarithm Equality/ is used.
223 proveEncryption ::
224 forall m r q.
225 Monad m => RandomGen r => SubGroup q =>
226 PublicKey q -> ZKP ->
227 ([Disjunction q],[Disjunction q]) ->
228 (EncryptionNonce q, Encryption q) ->
229 S.StateT r m (DisjProof q)
230 proveEncryption elecPubKey voterZKP (prevDisjs,nextDisjs) (encNonce,enc) = do
231 -- Fake proofs for all values except the correct one.
232 prevFakes <- fakeProof `mapM` prevDisjs
233 nextFakes <- fakeProof `mapM` nextDisjs
234 let prevProofs = fst <$> prevFakes
235 let nextProofs = fst <$> nextFakes
236 let challengeSum =
237 sum (proof_challenge <$> prevProofs) +
238 sum (proof_challenge <$> nextProofs)
239 let statement = encryptionStatement voterZKP enc
240 correctProof <- prove encNonce [groupGen, elecPubKey] $
241 -- 'Oracle'
242 \correctCommitments ->
243 let commitments =
244 foldMap snd prevFakes <>
245 correctCommitments <>
246 foldMap snd nextFakes in
247 hash statement commitments - challengeSum
248 return $ DisjProof $ prevProofs <> (correctProof : nextProofs)
249 where
250 fakeProof :: Disjunction q -> S.StateT r m (Proof q, [Commitment q])
251 fakeProof disj = do
252 -- Returns 'Commitment's verifiables by the verifier,
253 -- but computed from random 'proof_challenge' and 'proof_response'
254 -- instead of correct ones.
255 proof_challenge <- random
256 proof_response <- random
257 let proof = Proof{..}
258 return (proof, encryptionCommitments elecPubKey enc (disj, proof))
259
260 verifyEncryption ::
261 Monad m =>
262 SubGroup q =>
263 PublicKey q -> ZKP ->
264 [Disjunction q] ->
265 (Encryption q, DisjProof q) ->
266 Exn.ExceptT ErrorValidateEncryption m Bool
267 verifyEncryption elecPubKey voterZKP disjs (enc, DisjProof proofs)
268 | List.length proofs /= List.length disjs =
269 Exn.throwE $ ErrorValidateEncryption_InvalidProofLength
270 (fromIntegral $ List.length proofs)
271 (fromIntegral $ List.length disjs)
272 | otherwise = return $ challengeSum == hash (encryptionStatement voterZKP enc) commitments
273 where
274 challengeSum = sum (proof_challenge <$> proofs)
275 commitments = foldMap
276 (encryptionCommitments elecPubKey enc)
277 (List.zip disjs proofs)
278
279 -- ** Hashing
280 encryptionStatement :: SubGroup q => ZKP -> Encryption q -> BS.ByteString
281 encryptionStatement (ZKP voterZKP) Encryption{..} =
282 "prove|"<>voterZKP<>"|"
283 <> bytesNat encryption_nonce<>","
284 <> bytesNat encryption_vault<>"|"
285 -- NOTE: the commitment base 'elecPubKey' is notably absent here
286 -- despite it being used in 'encryptionCommitments',
287 -- maybe this is not necessary because it is already known
288 -- by every participant.
289
290 -- | @('encryptionCommitments' elecPubKey enc (disj,proof))@
291 -- returns the 'Commitment's with only the knowledge of the verifier.
292 --
293 -- The 'Proof' comes from 'prove' of @fakeProof@ in 'proveEncryption'.
294 encryptionCommitments ::
295 SubGroup q =>
296 PublicKey q -> Encryption q ->
297 (Disjunction q, Proof q) -> [G q]
298 encryptionCommitments elecPubKey Encryption{..} (disj, proof) =
299 [ commit proof groupGen encryption_nonce
300 -- == groupGen ^ nonce if 'Proof' comes from 'prove'
301 , commit proof elecPubKey (encryption_vault*disj)
302 -- == elecPubKey ^ nonce if 'Proof' comes from 'prove'
303 -- and 'encryption_vault' encrypts (- logBase groupGen disj).
304 ]
305
306 -- ** Type 'ErrorValidateEncryption'
307 -- | Error raised by 'verifyEncryption'.
308 data ErrorValidateEncryption
309 = ErrorValidateEncryption_InvalidProofLength Natural Natural
310 -- ^ When the number of proofs is different than
311 -- the number of 'Disjunction's.
312 deriving (Eq,Show)
313
314 -- * Type 'Question'
315 data Question q = Question
316 { question_text :: Text
317 , question_choices :: [Text]
318 , question_mini :: Opinion q
319 , question_maxi :: Opinion q
320 -- , question_blank :: Maybe Bool
321 } deriving (Eq, Show)
322
323 -- * Type 'Answer'
324 data Answer q = Answer
325 { answer_opinions :: [(Encryption q, DisjProof q)]
326 -- ^ Encrypted 'Opinion' for each 'question_choices'
327 -- with a 'DisjProof' that they belong to [0,1].
328 , answer_sumProof :: DisjProof q
329 -- ^ Proofs that the sum of the 'Opinon's encrypted in 'answer_opinions'
330 -- is an element of @[mini..maxi]@.
331 -- , answer_blankProof ::
332 } deriving (Eq,Show)
333
334 -- | @('encryptAnswer' elecPubKey zkp quest opinions)@
335 -- returns an 'Answer' validable by 'verifyAnswer',
336 -- unless an 'ErrorAnswer' is returned.
337 encryptAnswer ::
338 Monad m => RandomGen r => SubGroup q =>
339 PublicKey q -> ZKP ->
340 Question q -> [Bool] ->
341 S.StateT r (Exn.ExceptT ErrorAnswer m) (Answer q)
342 encryptAnswer elecPubKey zkp Question{..} opinionsBools
343 | not (question_mini <= opinionsSum && opinionsSum <= question_maxi) =
344 lift $ Exn.throwE $
345 ErrorAnswer_WrongSumOfOpinions
346 (nat opinionsSum)
347 (nat question_mini)
348 (nat question_maxi)
349 | List.length opinions /= List.length question_choices =
350 lift $ Exn.throwE $
351 ErrorAnswer_WrongNumberOfOpinions
352 (fromIntegral $ List.length opinions)
353 (fromIntegral $ List.length question_choices)
354 | otherwise = do
355 encryptions <- encrypt elecPubKey `mapM` opinions
356 individualProofs <- zipWithM
357 (\opinion -> proveEncryption elecPubKey zkp $
358 if opinion
359 then ([booleanDisjunctions List.!!0],[])
360 else ([],[booleanDisjunctions List.!!1]))
361 opinionsBools encryptions
362 sumProof <- proveEncryption elecPubKey zkp
363 ((List.tail <$>) $ List.genericSplitAt (nat (opinionsSum - question_mini)) $
364 intervalDisjunctions question_mini question_maxi)
365 ( sum (fst <$> encryptions) -- NOTE: sum the 'encNonce's
366 , sum (snd <$> encryptions) -- NOTE: sum the 'Encryption's
367 )
368 return $ Answer
369 { answer_opinions = List.zip
370 (snd <$> encryptions) -- NOTE: drop encNonce
371 individualProofs
372 , answer_sumProof = sumProof
373 }
374 where
375 opinionsSum = sum opinions
376 opinions = (\o -> if o then one else zero) <$> opinionsBools
377
378 verifyAnswer ::
379 SubGroup q =>
380 PublicKey q -> ZKP ->
381 Question q -> Answer q -> Bool
382 verifyAnswer elecPubKey zkp Question{..} Answer{..}
383 | List.length question_choices /= List.length answer_opinions = False
384 | otherwise = either (const False) id $ Exn.runExcept $ do
385 validOpinions <-
386 verifyEncryption elecPubKey zkp booleanDisjunctions
387 `traverse` answer_opinions
388 validSum <- verifyEncryption elecPubKey zkp
389 (intervalDisjunctions question_mini question_maxi)
390 ( sum (fst <$> answer_opinions)
391 , answer_sumProof )
392 return (and validOpinions && validSum)
393
394 -- ** Type 'ErrorAnswer'
395 -- | Error raised by 'encryptAnswer'.
396 data ErrorAnswer
397 = ErrorAnswer_WrongNumberOfOpinions Natural Natural
398 -- ^ When the number of opinions is different than
399 -- the number of choices ('question_choices').
400 | ErrorAnswer_WrongSumOfOpinions Natural Natural Natural
401 -- ^ When the sum of opinions is not within the bounds
402 -- of 'question_mini' and 'question_maxi'.
403 deriving (Eq,Show)
404
405 -- * Type 'Election'
406 data Election q = Election
407 { election_name :: Text
408 , election_description :: Text
409 , election_publicKey :: PublicKey q
410 , election_questions :: [Question q]
411 , election_uuid :: UUID
412 , election_hash :: Hash -- TODO: serialize to JSON to calculate this
413 } deriving (Eq,Show)
414
415 -- ** Type 'Hash'
416 newtype Hash = Hash Text
417 deriving (Eq,Ord,Show)
418
419 -- * Type 'Ballot'
420 data Ballot q = Ballot
421 { ballot_answers :: [Answer q]
422 , ballot_signature :: Maybe (Signature q)
423 , ballot_election_uuid :: UUID
424 , ballot_election_hash :: Hash
425 }
426
427 -- | @('encryptBallot' elec ('Just' secKey) opinionsByQuest)@
428 -- returns a 'Ballot' signed by 'secKey' (the voter's secret key)
429 -- where 'opinionsByQuest' is a list of 'Opinion's
430 -- on each 'question_choices' of each 'election_questions'.
431 encryptBallot ::
432 Monad m => RandomGen r => SubGroup q =>
433 Election q -> Maybe (SecretKey q) -> [[Bool]] ->
434 S.StateT r (Exn.ExceptT ErrorBallot m) (Ballot q)
435 encryptBallot Election{..} secKeyMay opinionsByQuest
436 | List.length election_questions /= List.length opinionsByQuest =
437 lift $ Exn.throwE $
438 ErrorBallot_WrongNumberOfAnswers
439 (fromIntegral $ List.length opinionsByQuest)
440 (fromIntegral $ List.length election_questions)
441 | otherwise = do
442 let (voterKeys, voterZKP) =
443 case secKeyMay of
444 Nothing -> (Nothing, ZKP "")
445 Just secKey ->
446 ( Just (secKey, pubKey)
447 , ZKP (bytesNat pubKey) )
448 where pubKey = publicKey secKey
449 ballot_answers <-
450 hoist (Exn.withExceptT ErrorBallot_Answer) $
451 zipWithM (encryptAnswer election_publicKey voterZKP)
452 election_questions opinionsByQuest
453 ballot_signature <- case voterKeys of
454 Nothing -> return Nothing
455 Just (secKey, signature_publicKey) -> do
456 signature_proof <-
457 prove secKey (Identity groupGen) $
458 \(Identity commitment) ->
459 hash
460 -- NOTE: the order is unusual, the commitments are first
461 -- then comes the statement. Best guess is that
462 -- this is easier to code due to their respective types.
463 (signatureCommitments voterZKP commitment)
464 (signatureStatement ballot_answers)
465 return $ Just Signature{..}
466 return Ballot
467 { ballot_answers
468 , ballot_election_hash = election_hash
469 , ballot_election_uuid = election_uuid
470 , ballot_signature
471 }
472
473 verifyBallot :: SubGroup q => Election q -> Ballot q -> Bool
474 verifyBallot Election{..} Ballot{..} =
475 ballot_election_uuid == election_uuid &&
476 ballot_election_hash == election_hash &&
477 List.length election_questions == List.length ballot_answers &&
478 let (isValidSign, zkpSign) =
479 case ballot_signature of
480 Nothing -> (True, ZKP "")
481 Just Signature{..} ->
482 let zkp = ZKP (bytesNat signature_publicKey) in
483 (, zkp) $
484 proof_challenge signature_proof == hash
485 (signatureCommitments zkp (commit signature_proof groupGen signature_publicKey))
486 (signatureStatement ballot_answers)
487 in
488 and $ isValidSign :
489 List.zipWith (verifyAnswer election_publicKey zkpSign)
490 election_questions ballot_answers
491
492 -- ** Type 'Signature'
493 -- | Schnorr-like signature.
494 --
495 -- Used by each voter to sign his/her encrypted 'Ballot'
496 -- using his/her 'Credential',
497 -- in order to avoid ballot stuffing.
498 data Signature q = Signature
499 { signature_publicKey :: PublicKey q
500 -- ^ Verification key.
501 , signature_proof :: Proof q
502 }
503
504 -- *** Hashing
505
506 -- | @('signatureStatement' answers)@
507 -- returns the encrypted material to be signed:
508 -- all the 'encryption_nonce's and 'encryption_vault's of the given @answers@.
509 signatureStatement :: Foldable f => SubGroup q => f (Answer q) -> [G q]
510 signatureStatement =
511 foldMap $ \Answer{..} ->
512 (`foldMap` answer_opinions) $ \(Encryption{..}, _proof) ->
513 [encryption_nonce, encryption_vault]
514
515 -- | @('signatureCommitments' voterZKP commitment)@
516 signatureCommitments :: SubGroup q => ZKP -> Commitment q -> BS.ByteString
517 signatureCommitments (ZKP voterZKP) commitment =
518 "sig|"<>voterZKP<>"|"<>bytesNat commitment<>"|"
519
520 -- ** Type 'ErrorBallot'
521 -- | Error raised by 'encryptBallot'.
522 data ErrorBallot
523 = ErrorBallot_WrongNumberOfAnswers Natural Natural
524 -- ^ When the number of answers
525 -- is different than the number of questions.
526 | ErrorBallot_Answer ErrorAnswer
527 -- ^ When 'encryptAnswer' raised an 'ErrorAnswer'.
528 deriving (Eq,Show)