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.Arithmetic
32 import Voting.Protocol.Cryptography
35 class Key crypto where
36 -- | Type of cryptography, eg. "FFC".
37 cryptoType :: crypto -> Text
38 -- | Name of the cryptographic paramaters, eg. "Belenios".
39 cryptoName :: crypto -> Text
40 -- | Generate a random 'SecretKey'.
43 Monad m => Random.RandomGen r =>
44 S.StateT r m (SecretKey crypto c)
45 -- | @('credentialSecretKey' uuid cred)@ returns the 'SecretKey'
46 -- derived from given 'uuid' and 'cred'
47 -- using 'Crypto.fastPBKDF2_SHA256'.
48 credentialSecretKey ::
50 UUID -> Credential -> SecretKey crypto c
51 -- | @('publicKey' secKey)@ returns the 'PublicKey'
52 -- derived from given 'SecretKey' @secKey@.
58 -- * Type 'Credential'
59 -- | A 'Credential' is a word of @('tokenLength'+1 '==' 15)@-characters
60 -- from a base alphabet of (@'tokenBase' '==' 58)@ characters:
61 -- "123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz"
62 -- (beware the absence of "0", \"O", \"I", and "l").
63 -- The last character is a checksum.
64 -- The entropy is: @('tokenLength' * log 'tokenBase' / log 2) '==' 82.01… bits@.
65 newtype Credential = Credential Text
66 deriving (Eq,Show,Generic)
67 deriving newtype NFData
68 deriving newtype JSON.ToJSON
69 instance JSON.FromJSON Credential where
70 parseJSON json@(JSON.String s) =
71 either (\err -> JSON.typeMismatch ("Credential: "<>show err) json) return $
73 parseJSON json = JSON.typeMismatch "Credential" json
75 credentialAlphabet :: [Char] -- TODO: make this an array
76 credentialAlphabet = "123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz"
78 tokenBase = List.length credentialAlphabet
82 -- | @'randomCredential'@ generates a random 'Credential'.
83 randomCredential :: Monad m => Random.RandomGen r => S.StateT r m Credential
85 rs <- replicateM tokenLength (randomR (fromIntegral tokenBase))
86 let (tot, cs) = List.foldl' (\(acc,ds) d ->
88 , charOfDigit d : ds )
90 let checksum = (negate tot + 53) `mod` 53 -- NOTE: why 53 and not 'tokenBase' ?
91 return $ Credential $ Text.reverse $ Text.pack (charOfDigit checksum:cs)
93 charOfDigit = (credentialAlphabet List.!!)
95 -- | @'readCredential'@ reads and check the well-formedness of a 'Credential'
97 readCredential :: Text -> Either ErrorToken Credential
99 | Text.length s /= tokenLength + 1 = Left ErrorToken_Length
102 (\acc c -> acc >>= \a -> ((a * tokenBase) +) <$> digitOfChar c)
105 checksum <- digitOfChar (Text.last s)
106 if (tot + checksum) `mod` 53 == 0
107 then Right (Credential s)
108 else Left ErrorToken_Checksum
111 maybe (Left $ ErrorToken_BadChar c) Right $
112 List.elemIndex c credentialAlphabet
114 -- ** Type 'ErrorToken'
116 = ErrorToken_BadChar Char.Char
117 | ErrorToken_Checksum
119 deriving (Eq,Show,Generic,NFData)
122 newtype UUID = UUID Text
123 deriving (Eq,Ord,Show,Generic)
124 deriving anyclass (JSON.ToJSON)
125 deriving newtype NFData
126 instance JSON.FromJSON UUID where
127 parseJSON json@(JSON.String s) =
128 either (\err -> JSON.typeMismatch ("UUID: "<>show err) json) return $
130 parseJSON json = JSON.typeMismatch "UUID" json
132 -- | @'randomUUID'@ generates a random 'UUID'.
135 Random.RandomGen r =>
138 rs <- replicateM tokenLength (randomR (fromIntegral tokenBase))
139 return $ UUID $ Text.pack $ charOfDigit <$> rs
141 charOfDigit = (credentialAlphabet List.!!)
143 -- | @'readCredential'@ reads and check the well-formedness of a 'Credential'
145 readUUID :: Text -> Either ErrorToken UUID
147 | Text.length s /= tokenLength = Left ErrorToken_Length
149 forM_ (Text.unpack s) digitOfChar
153 maybe (Left $ ErrorToken_BadChar c) Right $
154 List.elemIndex c credentialAlphabet