1 {-# LANGUAGE DeriveAnyClass #-}
2 {-# LANGUAGE DeriveGeneric #-}
3 {-# LANGUAGE DerivingStrategies #-}
4 module Voting.Protocol.Credential where
6 import Control.DeepSeq (NFData)
7 import Control.Monad (Monad(..), forM_, replicateM)
9 import Data.Char (Char)
10 import Data.Either (Either(..), either)
11 import Data.Eq (Eq(..))
12 import Data.Function (($))
13 import Data.Functor ((<$>))
15 import Data.Maybe (maybe)
16 import Data.Ord (Ord(..))
17 import Data.Reflection (Reifies(..))
18 import Data.Semigroup (Semigroup(..))
19 import Data.Text (Text)
20 import GHC.Generics (Generic)
21 import Prelude (Integral(..), fromIntegral)
22 import Text.Show (Show(..))
23 import qualified Control.Monad.Trans.State.Strict as S
24 import qualified Data.Aeson as JSON
25 import qualified Data.Aeson.Types as JSON
26 import qualified Data.Char as Char
27 import qualified Data.List as List
28 import qualified Data.Text as Text
29 import qualified System.Random as Random
31 import Voting.Protocol.Arith
34 class Key crypto where
35 -- | Type of cryptography, eg. "FFC".
36 cryptoType :: crypto -> Text
37 -- | Name of the cryptographic paramaters, eg. "Belenios".
38 cryptoName :: crypto -> Text
39 -- | Generate a random 'SecretKey'.
42 Monad m => Random.RandomGen r =>
43 S.StateT r m (SecretKey crypto c)
44 -- | @('credentialSecretKey' uuid cred)@ returns the 'SecretKey'
45 -- derived from given 'uuid' and 'cred'
46 -- using 'Crypto.fastPBKDF2_SHA256'.
47 credentialSecretKey ::
49 UUID -> Credential -> SecretKey crypto c
50 -- | @('publicKey' secKey)@ returns the 'PublicKey'
51 -- derived from given 'SecretKey' @secKey@.
57 -- ** Type 'PublicKey'
59 -- ** Type 'SecretKey'
62 -- * Type 'Credential'
63 -- | A 'Credential' is a word of @('tokenLength'+1 '==' 15)@-characters
64 -- from a base alphabet of (@'tokenBase' '==' 58)@ characters:
65 -- "123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz"
66 -- (beware the absence of "0", \"O", \"I", and "l").
67 -- The last character is a checksum.
68 -- The entropy is: @('tokenLength' * log 'tokenBase' / log 2) '==' 82.01… bits@.
69 newtype Credential = Credential Text
70 deriving (Eq,Show,Generic)
71 deriving newtype NFData
72 deriving newtype JSON.ToJSON
73 instance JSON.FromJSON Credential where
74 parseJSON json@(JSON.String s) =
75 either (\err -> JSON.typeMismatch ("Credential: "<>show err) json) return $
77 parseJSON json = JSON.typeMismatch "Credential" json
79 credentialAlphabet :: [Char] -- TODO: make this an array
80 credentialAlphabet = "123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz"
82 tokenBase = List.length credentialAlphabet
86 -- | @'randomCredential'@ generates a random 'Credential'.
87 randomCredential :: Monad m => Random.RandomGen r => S.StateT r m Credential
89 rs <- replicateM tokenLength (randomR (fromIntegral tokenBase))
90 let (tot, cs) = List.foldl' (\(acc,ds) d ->
92 , charOfDigit d : ds )
94 let checksum = (neg tot + 53) `mod` 53 -- NOTE: why 53 and not 'tokenBase' ?
95 return $ Credential $ Text.reverse $ Text.pack (charOfDigit checksum:cs)
97 charOfDigit = (credentialAlphabet List.!!)
99 -- | @'readCredential'@ reads and check the well-formedness of a 'Credential'
101 readCredential :: Text -> Either ErrorToken Credential
103 | Text.length s /= tokenLength + 1 = Left ErrorToken_Length
106 (\acc c -> acc >>= \a -> ((a * tokenBase) +) <$> digitOfChar c)
109 checksum <- digitOfChar (Text.last s)
110 if (tot + checksum) `mod` 53 == 0
111 then Right (Credential s)
112 else Left ErrorToken_Checksum
115 maybe (Left $ ErrorToken_BadChar c) Right $
116 List.elemIndex c credentialAlphabet
118 -- ** Type 'ErrorToken'
120 = ErrorToken_BadChar Char.Char
121 | ErrorToken_Checksum
123 deriving (Eq,Show,Generic,NFData)
126 newtype UUID = UUID Text
127 deriving (Eq,Ord,Show,Generic)
128 deriving anyclass (JSON.ToJSON)
129 deriving newtype NFData
130 instance JSON.FromJSON UUID where
131 parseJSON json@(JSON.String s) =
132 either (\err -> JSON.typeMismatch ("UUID: "<>show err) json) return $
134 parseJSON json = JSON.typeMismatch "UUID" json
136 -- | @'randomUUID'@ generates a random 'UUID'.
139 Random.RandomGen r =>
142 rs <- replicateM tokenLength (randomR (fromIntegral tokenBase))
143 return $ UUID $ Text.pack $ charOfDigit <$> rs
145 charOfDigit = (credentialAlphabet List.!!)
147 -- | @'readCredential'@ reads and check the well-formedness of a 'Credential'
149 readUUID :: Text -> Either ErrorToken UUID
151 | Text.length s /= tokenLength = Left ErrorToken_Length
153 forM_ (Text.unpack s) digitOfChar
157 maybe (Left $ ErrorToken_BadChar c) Right $
158 List.elemIndex c credentialAlphabet