1 module Protocol.Credential where
3 import Control.Monad (Monad(..), replicateM)
6 import Data.Char (Char)
7 import Data.Either (Either(..))
8 import Data.Eq (Eq(..))
9 import Data.Function (($))
10 import Data.Functor ((<$>))
12 import Data.Maybe (maybe)
13 import Data.Ord (Ord(..))
14 import Data.Text (Text)
15 import Numeric.Natural (Natural)
16 import Prelude (Integral(..), fromIntegral, div)
17 import Text.Show (Show)
18 import qualified Control.Monad.Trans.State.Strict as S
19 import qualified Crypto.KDF.PBKDF2 as Crypto
20 import qualified Data.ByteArray as ByteArray
21 import qualified Data.ByteString as BS
22 import qualified Data.Char as Char
23 import qualified Data.List as List
24 import qualified Data.Text as Text
25 import qualified Data.Text.Encoding as Text
26 import qualified System.Random as Random
28 import Protocol.Arithmetic
30 -- * Type 'Credential'
31 -- | A 'Credential' is a word of @('tokenLength'+1 '==' 15)@-characters
32 -- from a base alphabet of (@'tokenBase' '==' 58)@ characters:
33 -- "123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz"
34 -- (beware the absence of "0", \"O", \"I", and "l").
35 -- The last character is a checksum.
36 -- The entropy is: @('tokenLength' * log 'tokenBase' / log 2) '==' 82.01… bits@.
37 newtype Credential = Credential Text
40 credentialAlphabet :: [Char] -- TODO: make this an array
41 credentialAlphabet = "123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz"
43 tokenBase = List.length credentialAlphabet
47 -- | @'randomCredential'@ generates a random 'Credential'.
51 S.StateT r m Credential
53 rs <- replicateM tokenLength (randomR (fromIntegral tokenBase))
54 let (tot, cs) = List.foldl' (\(acc,ds) d ->
56 , charOfDigit d : ds )
58 let checksum = (neg tot + 53) `mod` 53 -- NOTE: why 53 and not 'tokenBase' ?
59 return $ Credential $ Text.reverse $ Text.pack (charOfDigit checksum:cs)
61 charOfDigit = (credentialAlphabet List.!!)
63 -- | @'readCredential'@ reads and check the well-formedness of a 'Credential'
65 readCredential :: Text -> Either CredentialError Credential
67 | Text.length s /= tokenLength + 1 = Left CredentialError_Length
70 (\acc c -> acc >>= \a -> ((a * tokenBase) +) <$> digitOfChar c)
73 checksum <- digitOfChar (Text.last s)
74 if (tot + checksum) `mod` 53 == 0
75 then Right (Credential s)
76 else Left CredentialError_Checksum
79 maybe (Left $ CredentialError_BadChar c) Right $
80 List.elemIndex c credentialAlphabet
82 -- ** Type 'CredentialError'
84 = CredentialError_BadChar Char.Char
85 | CredentialError_Checksum
86 | CredentialError_Length
90 newtype UUID = UUID Text
91 deriving (Eq,Ord,Show)
93 -- | @'randomUUID'@ generates a random 'UUID'.
99 rs <- replicateM tokenLength (randomR (fromIntegral tokenBase))
100 let (tot, cs) = List.foldl' (\(acc,ds) d ->
101 ( acc * tokenBase + d
102 , charOfDigit d : ds )
104 return $ UUID $ Text.reverse $ Text.pack cs
106 charOfDigit = (credentialAlphabet List.!!)
108 -- ** Type 'SecretKey'
111 -- | @('secretKey' uuid cred)@ returns the 'SecretKey'
112 -- derived from given 'uuid' and 'cred'
113 -- using 'Crypto.fastPBKDF2_SHA256'.
114 secretKey :: SubGroup q => UUID -> Credential -> SecretKey q
115 secretKey (UUID uuid) (Credential cred) =
117 (\acc b -> acc`shiftL`3 + fromIntegral b)
119 (ByteArray.convert deriv)
121 deriv :: BS.ByteString
123 Crypto.fastPBKDF2_SHA256
125 { Crypto.iterCounts = 1000
126 , Crypto.outputLength = 256 `div` 8
128 (Text.encodeUtf8 cred)
129 (Text.encodeUtf8 uuid)
131 -- ** Type 'PublicKey'
134 -- | @('publicKey' secKey)@ returns the 'PublicKey'
135 -- derived from given 'SecretKey' in @('SubGroup' q)@.
136 publicKey :: SubGroup q => SecretKey q -> PublicKey q
137 publicKey = (groupGen ^)