{-| Module: Elocrypt.Passowrd Description: Generate pronouncable, hard-to-guess passwords Copyright: (c) Sean Gillespie, 2015 License: OtherLicense Maintainer: Sean Gillespie <sean@mistersg.net> Stability: Experimental Generate easy-to-remember, hard-to-guess passwords -} module Data.Elocrypt where import Data.Elocrypt.Trigraph import Data.Elocrypt.Utils import Control.Monad import Control.Monad.Random hiding (next) import Data.Bool import Data.Char import Data.List (findIndices) import Data.Maybe import Data.Ratio import Prelude hiding (min, max) import qualified Data.Map as M -- * Data Types -- |Options for generating passwords or passphrases. Do not use -- this constructor directly. Instead use 'genOptions' to construct -- an instance. data GenOptions = GenOptions { genCapitals :: Bool, genDigits :: Bool, genSpecials :: Bool } deriving (Eq, Show) -- |Default options for generating passwords or passphrases. This is -- the preferred way to construct 'GenOptions'. genOptions :: GenOptions genOptions = GenOptions { genCapitals = False, genDigits = False, genSpecials = False } -- * Random password generators -- |Generate a password using the generator g, returning the result and the -- updated generator. -- -- @ -- -- Generate a password of length 10 using the system generator -- myGenPassword :: IO (String, StdGen) -- myGenPassword = genPassword 10 genOptions \`liftM\` getStdGen -- @ genPassword :: RandomGen g => Int -- ^ password length -> GenOptions -- ^ options -> g -- ^ random generator -> (String, g) genPassword len = runRand . mkPassword len -- |Plural version of genPassword. Generates an infinite list of passwords -- using the generator g, returning the result and the updated generator. -- -- @ -- -- Generate 10 passwords of length 10 using the system generator -- myGenPasswords :: IO ([String], StdGen) -- myGenPasswords = (\(ls, g) -> (ls, g) `liftM` genPasswords 10 10 genOptions `liftM` getStdGen -- @ genPasswords :: RandomGen g => Int -- ^ password length -> Int -- ^ number of passwords -> GenOptions -- ^ options -> g -- ^ random generator -> ([String], g) genPasswords len n = runRand . mkPasswords len n -- |Generate a password using the generator g, returning the result. -- -- @ -- -- Generate a password of length 10 using the system generator -- myNewPassword :: IO String -- myNewPassword = newPassword 10 genOptions \`liftM\` getStdGen -- @ newPassword :: RandomGen g => Int -- ^ password length -> GenOptions -- ^ options -> g -- ^ random generator -> String newPassword len = evalRand . mkPassword len -- |Plural version of newPassword. Generates an infinite list of passwords -- using the generator g, returning the result -- -- @ -- -- Generate 10 passwords of length 10 using the system generator -- myNewPasswords :: IO [String] -- myNewPasswords = genPasswords 10 10 genOptions `liftM` getStdGen -- @ newPasswords :: RandomGen g => Int -- ^ password length -> Int -- ^ number of passwords -> GenOptions -- ^ options -> g -- ^ random generator -> [String] newPasswords len n = evalRand . mkPasswords len n -- |Generate a password using the MonadRandom m. MonadRandom is exposed here -- for extra control. -- -- @ -- -- Generate a password of length 10 using the system generator -- myPassword :: IO String -- myPassword = evalRand (mkPassword 10 genOptions) \`liftM\` getStdGen -- @ mkPassword :: MonadRandom m => Int -- ^ password length -> GenOptions -- ^ options -> m String mkPassword len opts = do f2 <- first2 let f2' = reverse f2 pass <- if len > 2 then lastN (len - 2) f2' else return (take len f2') let pass' = reverse pass -- Apply the transformations in order, if appropriate options specified let ms = [(genCapitals opts, capitalizeR), (genDigits opts, numerizeR), (genSpecials opts, specializeR) ] foldM (\p f -> f len p) pass' . map snd . filter fst $ ms -- |Plural version of mkPassword. Generate an infinite list of passwords using -- the MonadRandom m. MonadRandom is exposed here for extra control. -- -- @ -- -- Generate an list of length 20 with passwords of length 10 using the system generator -- myMkPasswords :: IO [String] -- myMkPasswords = evalRand (mkPasswords 10 20 genOptions) \`liftM\` getStdGen -- @ mkPasswords :: MonadRandom m => Int -- ^ password length -> Int -- ^ number of passwords -> GenOptions -- ^ options -> m [String] mkPasswords len n = replicateM n . mkPassword len -- * Random passphrase generators -- |Generate a passphrase using the generator g, returning the result and the -- updated generator. -- -- @ -- -- Generate a passphrase of 10 words, each having a length between 6 and 12, -- -- using the system generator -- myGenPassphrase :: IO (String, StdGen) -- myGenPassphrase = genPassword 10 6 10 genOptions \`liftM\` getStdGen -- @ genPassphrase :: RandomGen g => Int -- ^ number of words -> Int -- ^ minimum word length -> Int -- ^ maximum word length -> GenOptions -- ^ options -> g -- ^ random generator -> ([String], g) genPassphrase n min max = runRand . mkPassphrase n min max -- |Generate a passphrase using the generator g, returning the result. -- -- @ -- -- Generate a passphrase of 10 words, each having a length between 6 an 12, -- -- using the system generator. -- myNewPassphrase :: IO String -- myNewPassphrase = newPassphrase 10 6 12 \`liftM\` getStdGen -- @ newPassphrase :: RandomGen g => Int -- ^ number of words -> Int -- ^ minimum word length -> Int -- ^ maximum word length -> GenOptions -- ^ options -> g -- ^ random generator -> [String] newPassphrase n min max = evalRand . mkPassphrase n min max -- |Generate a finite number of words of random length (between @min@ and @max@ chars) -- using the MonadRandom m. MonadRandom is exposed here for extra control. -- -- @ -- -- Generate a passphrase of 10 words, each having a length between 6 and 12. -- myPassphrase :: IO String -- myPassphrase = evalRand (mkPassphrase 10 6 12) \`liftM\` getStdGen -- @ mkPassphrase :: MonadRandom m => Int -- ^ number of words -> Int -- ^ minimum word length -> Int -- ^ maximum word length -> GenOptions -- ^ options -> m [String] mkPassphrase n min max opts = replicateM n $ getRandomR (min, max) >>= flip mkPassword opts -- * Internal -- |Generate two random characters. Uses 'Elocrypt.Trigraph.trigragh' -- to generate a weighted list. first2 :: MonadRandom m => m String first2 = fromList (map toWeight frequencies) where toWeight (s, w) = (s, sum w) -- |Generate the last n characters using previous two characters -- and their 'Elocrypt.Trigraph.trigraph' lastN :: MonadRandom m => Int -> String -> m String lastN 0 ls = return ls lastN len ls = next ls >>= lastN (len - 1) . (: ls) -- |Generate a random character based on the previous two characters and -- their 'Elocrypt.Trigraph.trigraph' next :: MonadRandom m => String -- ^ the prefix -> m Char next = fromList . fromJust . findWeights . reverse . take 2 -- |Randomly capitalize at least 1 character. Additional characters capitalize -- at a probability of 1/12 capitalizeR :: MonadRandom m => Int -> String -> m String capitalizeR len s = capitalize s >>= capitalize1 len where capitalize = updateR (return . toUpper) (1 % 12) -- |Randomly capitalize 1 character capitalize1 :: MonadRandom m => Int -> String -> m String capitalize1 len s = getRandomR (0, len - 1) >>= capitalize1' s where capitalize1' = update1 (return . toUpper) -- |Randomly numerize at least 1 character. Additional characters numerize -- at a probability of 1/6 numerizeR :: MonadRandom m => Int -> String -> m String numerizeR len s = numerize s >>= numerize1 len where numerize = updateR (uniform . toDigit) (1 % 6) numerize1 :: MonadRandom m => Int -> String -> m String numerize1 len s | null candidates = return s | otherwise = uniform candidates >>= numerize1' s where candidates = findIndices (`elem` M.keys numeralConversions) s numerize1' = update1 (uniform . toDigit) -- |Randomly make at least 1 character a symbol. Additional characters specialize -- at a probability of 1/4 specializeR :: MonadRandom m => Int -> String -> m String specialize1 :: MonadRandom m => Int -> String -> m String specializeR len s = specialize s >>= specialize1 len where specialize = updateR (uniform . toSymbol) (1 % 6) specialize1 len s | null candidates = return s | otherwise = uniform candidates >>= specialize1' s where candidates = findIndices (`elem` M.keys symbolConversions) s specialize1' = update1 (uniform . toSymbol)