module Crypto.Password
( CharType(..)
, PasswordFeature(..)
, generatePassword
, validatePassword
, formatCharType
, passwordFeatureMessage
) where
import qualified Control.Monad.Random as Random (fromList)
import Data.List (foldl')
import Data.Map (Map)
import qualified Data.Map as Map (empty, elems, insert, lookup, fromList, toList)
import Data.Maybe (fromMaybe)
import System.Random (randomRIO)
data CharType = Lowercase
| Uppercase
| Digit
| Symbol
deriving (Eq, Show)
instance Ord CharType where
compare Lowercase Uppercase = LT
compare Lowercase Digit = LT
compare Lowercase Symbol = LT
compare Uppercase Digit = LT
compare Uppercase Symbol = LT
compare Digit Symbol = LT
compare x y | x == y = EQ
| otherwise = GT
lowercaseChars = "abcdefghijkpqrstuvwxyz"
uppercaseChars = "ABCDEFGHIJKLMNOPQRSTUVWXYZ"
digitChars = "0123456789"
symbolChars = "!\"#$%&'()*+,-./:;<=>?@[\\]^_`{|}~"
isCharType :: CharType -> Char -> Bool
isCharType Lowercase = (`elem` lowercaseChars)
isCharType Uppercase = (`elem` uppercaseChars)
isCharType Digit = (`elem` digitChars)
isCharType Symbol = (`elem` symbolChars)
formatCharType :: CharType -> String
formatCharType Lowercase = "lowercase character(s)"
formatCharType Uppercase = "uppercase character(s)"
formatCharType Digit = "digit(s)"
formatCharType Symbol = "symbol character(s)"
randomElement :: [a] -> IO a
randomElement elems = fmap (elems!!) $ randomRIO (0, (length elems 1))
genRandom :: CharType -> IO Char
genRandom Lowercase = randomElement lowercaseChars
genRandom Uppercase = randomElement uppercaseChars
genRandom Digit = randomElement digitChars
genRandom Symbol = randomElement symbolChars
data PasswordFeature = Length Int
| Include CharType
| IncludeAtLeast Int CharType
deriving (Eq, Show)
passwordFeatureMessage :: PasswordFeature -> String
passwordFeatureMessage (Length x) = "should be at least " ++ show x ++ " character(s) long"
passwordFeatureMessage (Include t) = "should include " ++ formatCharType t
passwordFeatureMessage (IncludeAtLeast x t) = "should have at least " ++ show x ++ " " ++ formatCharType t
data PasswordGenState = PasswordGenState (Map CharType Int) Int Int
newPasswordGenState m l =
PasswordGenState m (sum . Map.elems $ m) l
generatePassword :: [PasswordFeature] -> IO String
generatePassword features = generate (newPasswordGenState minCounts len) ""
where generate :: PasswordGenState -> String -> IO String
generate (PasswordGenState _ _ 0) password = return password
generate (PasswordGenState m min left) password = do
let weightFunc = if left > min then defaultWeights else countWeights
charType <- Random.fromList $ map weightFunc $ Map.toList m
let left' = left 1
let charTypeCount = fromMaybe 0 $ Map.lookup charType m
newState = if (charTypeCount == 0)
then PasswordGenState m min left'
else PasswordGenState (Map.insert charType (charTypeCount1) m) (min1) left'
c <- genRandom charType
generate newState (c:password)
where countWeights (k, v) = (k, toRational v)
defaultWeights (k, v) = (k, defaultCharTypeWeight k)
defaultCharTypeWeight Lowercase = 3
defaultCharTypeWeight _ = 1
len = case filter isLength features of
[] -> 8
(Length x:_) -> x
where isLength :: PasswordFeature -> Bool
isLength (Length _) = True
isLength _ = False
minCounts :: Map CharType Int
minCounts = let counts = foldl' updateCountsWithFeature Map.empty features
in if (counts == Map.empty) then defaultCounts else counts
where updateCountsWithFeature m (Include t) = Map.insert t 0 m
updateCountsWithFeature m (IncludeAtLeast x t) = Map.insert t x m
updateCountsWithFeature m _ = m
defaultCounts = Map.fromList [(Lowercase, 0), (Digit, 0)]
validatePassword :: [PasswordFeature]
-> String
-> Either PasswordFeature ()
validatePassword features password = foldl (>>) (Right ()) $ map (checkEither password) features
where checkEither :: String -> PasswordFeature -> Either PasswordFeature ()
checkEither s c = if check s c then Right () else Left c
check :: String -> PasswordFeature -> Bool
check s (Length x) = length s >= x
check s (Include t) = True
check s (IncludeAtLeast x t) = count (isCharType t) s >= x
count :: (a -> Bool) -> [a] -> Int
count p xs = length . filter p $ xs