packages feed

password 2.0.1.1 → 2.1.0.0

raw patch · 7 files changed

+1050/−7 lines, 7 filesdep +tasty-hunitPVP ok

version bump matches the API change (PVP)

Dependencies added: tasty-hunit

API changes (from Hackage documentation)

+ Data.Password.Validate: CategoryAmountsAboveMaxLength :: !MaximumLength -> !Int -> InvalidPolicyReason
+ Data.Password.Validate: CharSetPredicate :: (Char -> Bool) -> CharSetPredicate
+ Data.Password.Validate: Digit :: CharacterCategory
+ Data.Password.Validate: InvalidCharSetPredicate :: !CharacterCategory -> !MinimumAmount -> InvalidPolicyReason
+ Data.Password.Validate: InvalidCharacters :: !Text -> InvalidReason
+ Data.Password.Validate: InvalidLength :: !MinimumLength -> !MaximumLength -> InvalidPolicyReason
+ Data.Password.Validate: InvalidPassword :: [InvalidReason] -> ValidationResult
+ Data.Password.Validate: Lowercase :: CharacterCategory
+ Data.Password.Validate: MaxLengthBelowZero :: !MaximumLength -> InvalidPolicyReason
+ Data.Password.Validate: NotEnoughReqChars :: !CharacterCategory -> !MinimumAmount -> !ProvidedAmount -> InvalidReason
+ Data.Password.Validate: PasswordPolicy :: !Int -> !Int -> !Int -> !Int -> !Int -> !Int -> CharSetPredicate -> PasswordPolicy
+ Data.Password.Validate: PasswordTooLong :: !MaximumLength -> !ProvidedLength -> InvalidReason
+ Data.Password.Validate: PasswordTooShort :: !MinimumLength -> !ProvidedLength -> InvalidReason
+ Data.Password.Validate: Special :: CharacterCategory
+ Data.Password.Validate: Uppercase :: CharacterCategory
+ Data.Password.Validate: ValidPassword :: ValidationResult
+ Data.Password.Validate: [charSetPredicate] :: PasswordPolicy -> CharSetPredicate
+ Data.Password.Validate: [digitChars] :: PasswordPolicy -> !Int
+ Data.Password.Validate: [getCharSetPredicate] :: CharSetPredicate -> Char -> Bool
+ Data.Password.Validate: [lowercaseChars] :: PasswordPolicy -> !Int
+ Data.Password.Validate: [maximumLength] :: PasswordPolicy -> !Int
+ Data.Password.Validate: [minimumLength] :: PasswordPolicy -> !Int
+ Data.Password.Validate: [specialChars] :: PasswordPolicy -> !Int
+ Data.Password.Validate: [uppercaseChars] :: PasswordPolicy -> !Int
+ Data.Password.Validate: allButCSP :: PasswordPolicy -> [Int]
+ Data.Password.Validate: categoryToPredicate :: CharacterCategory -> Char -> Bool
+ Data.Password.Validate: data CharacterCategory
+ Data.Password.Validate: data InvalidPolicyReason
+ Data.Password.Validate: data InvalidReason
+ Data.Password.Validate: data PasswordPolicy
+ Data.Password.Validate: data ValidPasswordPolicy
+ Data.Password.Validate: data ValidationResult
+ Data.Password.Validate: defaultCharSet :: String
+ Data.Password.Validate: defaultCharSetPredicate :: CharSetPredicate
+ Data.Password.Validate: defaultPasswordPolicy :: PasswordPolicy
+ Data.Password.Validate: defaultPasswordPolicy_ :: ValidPasswordPolicy
+ Data.Password.Validate: fromValidPasswordPolicy :: ValidPasswordPolicy -> PasswordPolicy
+ Data.Password.Validate: instance GHC.Classes.Eq Data.Password.Validate.CharacterCategory
+ Data.Password.Validate: instance GHC.Classes.Eq Data.Password.Validate.InvalidPolicyReason
+ Data.Password.Validate: instance GHC.Classes.Eq Data.Password.Validate.InvalidReason
+ Data.Password.Validate: instance GHC.Classes.Eq Data.Password.Validate.PasswordPolicy
+ Data.Password.Validate: instance GHC.Classes.Eq Data.Password.Validate.ValidPasswordPolicy
+ Data.Password.Validate: instance GHC.Classes.Eq Data.Password.Validate.ValidationResult
+ Data.Password.Validate: instance GHC.Classes.Ord Data.Password.Validate.CharacterCategory
+ Data.Password.Validate: instance GHC.Classes.Ord Data.Password.Validate.InvalidPolicyReason
+ Data.Password.Validate: instance GHC.Classes.Ord Data.Password.Validate.InvalidReason
+ Data.Password.Validate: instance GHC.Classes.Ord Data.Password.Validate.PasswordPolicy
+ Data.Password.Validate: instance GHC.Classes.Ord Data.Password.Validate.ValidPasswordPolicy
+ Data.Password.Validate: instance GHC.Show.Show Data.Password.Validate.CharacterCategory
+ Data.Password.Validate: instance GHC.Show.Show Data.Password.Validate.InvalidPolicyReason
+ Data.Password.Validate: instance GHC.Show.Show Data.Password.Validate.InvalidReason
+ Data.Password.Validate: instance GHC.Show.Show Data.Password.Validate.PasswordPolicy
+ Data.Password.Validate: instance GHC.Show.Show Data.Password.Validate.ValidPasswordPolicy
+ Data.Password.Validate: instance GHC.Show.Show Data.Password.Validate.ValidationResult
+ Data.Password.Validate: isSpecial :: Char -> Bool
+ Data.Password.Validate: isValidPassword :: ValidPasswordPolicy -> Password -> Bool
+ Data.Password.Validate: newtype CharSetPredicate
+ Data.Password.Validate: type MaximumLength = Int
+ Data.Password.Validate: type MinimumAmount = Int
+ Data.Password.Validate: type MinimumLength = Int
+ Data.Password.Validate: type ProvidedAmount = Int
+ Data.Password.Validate: type ProvidedLength = Int
+ Data.Password.Validate: validateCharSetPredicate :: PasswordPolicy -> [InvalidPolicyReason]
+ Data.Password.Validate: validatePassword :: ValidPasswordPolicy -> Password -> ValidationResult
+ Data.Password.Validate: validatePasswordPolicy :: PasswordPolicy -> Either [InvalidPolicyReason] ValidPasswordPolicy
+ Data.Password.Validate: validatePasswordPolicyTH :: PasswordPolicy -> Q Exp

Files

ChangeLog.md view
@@ -1,9 +1,20 @@ # Changelog for password +## 2.1.0.0++-   A new `Validate` module has been added to dictate policies that passwords+    should adhere to and the necessary API to verify that they do.+    [#26](https://github.com/cdepillabout/password/pull/26)+    Huge thanks to [@HirotoShioi](https://github.com/HirotoShioi) for picking+    up the task of adding this functionality and doing most of the groundwork.+    [#27](https://github.com/cdepillabout/password/pull/27)+    Thanks to Felix Paulusma ([@Vlix](https://github.com/Vlix)) for finishing+    up the API and documentation.+ ## 2.0.1.1 --   Fixed cross-module links in the haddocks-    [#19](https://github.com/cdepillabout/password/pull/19).  Thanks to+-   Fixed cross-module links in the haddocks.+    [#19](https://github.com/cdepillabout/password/pull/19) Thanks to     [@TristanCacqueray](https://github.com/TristanCacqueray) for fixing this.  ## 2.0.1.0@@ -11,8 +22,7 @@ -   Switched checking hashes to using `Data.ByteArray.constEq`, instead of     the default `(==)` method of `ByteString`. This is to make it more secure     against timing attacks. [#16](https://github.com/cdepillabout/password/pull/16)-    Thanks to maralorn ([@maralorn](https://github.com/maralorn)) for bringing-    this up.+    Thanks to [@maralorn](https://github.com/maralorn) for bringing this up.  ## 2.0.0.1 
password.cabal view
@@ -1,7 +1,7 @@ cabal-version: 1.12  name:           password-version:        2.0.1.1+version:        2.1.0.0 category:       Data synopsis:       Hashing and checking of passwords description:    A library providing functionality for working with plain-text and hashed passwords with different types of algorithms.@@ -36,6 +36,7 @@       Data.Password.Bcrypt       Data.Password.PBKDF2       Data.Password.Scrypt+      Data.Password.Validate   other-modules:       Paths_password       Data.Password.Internal@@ -43,8 +44,9 @@       base        >= 4.9      && < 5     , base64      >= 0.3      && < 0.5     , bytestring  >= 0.10.8.1 && < 0.11-    , cryptonite  >= 0.15.1   && < 0.27+    , cryptonite  >= 0.15.1   && < 0.28     , memory      >= 0.14     && < 0.16+    , template-haskell     , text        >= 1.2.2    && < 1.3   ghc-options:       -Wall@@ -84,6 +86,8 @@     , Internal     , PBKDF2     , Scrypt+    , TestPolicy+    , Validate     , Paths_password   ghc-options:       -threaded -O2 -rtsopts -with-rtsopts=-N@@ -96,6 +100,7 @@     , quickcheck-instances     , scrypt     , tasty+    , tasty-hunit     , tasty-quickcheck     , text   default-language:
src/Data/Password/Internal.hs view
@@ -141,4 +141,3 @@ showT :: forall a. Show a => a -> Text showT = T.pack . show {-# INLINE showT #-}-
+ src/Data/Password/Validate.hs view
@@ -0,0 +1,569 @@+{-# LANGUAGE CPP               #-}+{-# LANGUAGE LambdaCase        #-}+{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE RecordWildCards   #-}+{-# LANGUAGE TemplateHaskell   #-}++{-|+Module      : Data.Password.Validate+Copyright   : (c) Hiroto Shioi, 2020; Felix Paulusma, 2020+License     : BSD-style (see LICENSE file)+Maintainer  : cdep.illabout@gmail.com+Stability   : experimental+Portability : POSIX++= Password Validation++It is common for passwords to have a set of requirements. For example,+a password might have to contain at least a certain amount of characters+that consist of uppercase and lowercase alphabetic characters combined with+numbers and/or other special characters.++This module provides an API which enables you to set up your own+'PasswordPolicy' to validate the format of 'Password's.++== Password Policies++The most important part is to have a valid and robust 'PasswordPolicy'.++A 'defaultPasswordPolicy_' is provided to quickly set up a "good-enough"+validation of passwords, but you can also adjust it, or just create your+own.++Just remember that a 'PasswordPolicy' must be validated first to make+sure it is actually a 'ValidPasswordPolicy'. Otherwise, you'd never be+able to validate any given 'Password's.++= Example usage++So let's say we're fine with the default policy, which requires the+password to be between 8-64 characters, and have at least one lowercase,+one uppercase and one digit character, then our function would look like+the following:++@+myValidateFunc :: 'Password' -> Bool+myValidateFunc = 'isValidPassword' 'defaultPasswordPolicy_'+@++== Custom policies++But, for example, if you'd like to enforce that a 'Password' includes+at least one special character, and be at least 12 characters long,+you'll have to make your own 'PasswordPolicy'.++@+customPolicy :: 'PasswordPolicy'+customPolicy =+  'defaultPasswordPolicy'+    { minimumLength = 12+    , specialChars = 1+    }+@++This custom policy will then have to be validated first, so it can be+used to validate 'Password's further on.++== Template Haskell++The easiest way to validate a custom 'PasswordPolicy' is by using a+Template Haskell splice.+Just turn on the @\{\-\# LANGUAGE TemplateHaskell \#\-\}@ pragma, pass your+policy to 'validatePasswordPolicyTH', surround it by @\$(...)@ and if+it compiles it will be a 'ValidPasswordPolicy'.++@+{-\# LANGUAGE TemplateHaskell \#-}+customValidPolicy :: 'ValidPasswordPolicy'+customValidPolicy = $('validatePasswordPolicyTH' customPolicy)+@++__NB: any custom 'CharSetPredicate' will be ignored by 'validatePasswordPolicyTH'__+__and replaced with the 'defaultCharSetPredicate'.__+So if you want to use your own 'CharSetPredicate', you won't be able+to validate your policy using 'validatePasswordPolicyTH'. Most users,+however, will find 'defaultCharSetPredicate' to be sufficient.++== At runtime++Another way of validating your custom policy is 'validatePasswordPolicy'.+In an application, this might be implemented in the following way.++@+main :: IO ()+main =+    case ('validatePasswordPolicy' customPolicy) of+      Left reasons -> error $ show reasons+      Right validPolicy -> app \`runReaderT\` validPolicy++customValidateFunc :: 'Password' -> ReaderT 'ValidPasswordPolicy' IO Bool+customValidateFunc pwd = do+    policy <- ask+    return $ 'isValidPassword' policy pwd+@++== Let's get dangerous++Or, if you like living on the edge, you could also just match on 'Right'.+I hope you're certain your policy is valid, though. So please have at least+a unit test to verify that passing your 'PasswordPolicy' to+'validatePasswordPolicy' actually returns a 'Right'.++@+Right validPolicy = 'validatePasswordPolicy' customPolicy++customValidateFunc :: 'Password' -> Bool+customValidateFunc = 'isValidPassword' validPolicy+@++-}++module Data.Password.Validate+  ( -- * Validating passwords+    --+    -- |+    -- The main function of this module is probably 'isValidPassword',+    -- as it is simple and straightforward.+    --+    -- Though if you'd want to know why a 'Password' failed to validate,+    -- because you'd maybe like to communicate those 'InvalidReason's+    -- back to the user, 'validatePassword' is here to help you out.+    validatePassword,+    isValidPassword,+    ValidationResult(..),+    -- ** Password Policy+    --+    -- |+    -- A 'PasswordPolicy' has to be validated before it can be used to validate a+    -- 'Password'.+    -- This is done using 'validatePasswordPolicy' or 'validatePasswordPolicyTH'.+    --+    -- Next to the obvious lower and upper bounds for the length of a 'Password',+    -- a 'PasswordPolicy' can dictate how many lowercase letters, uppercase letters,+    -- digits and/or special characters are minimally required to be used in the+    -- 'Password' to be considered a valid 'Password'.+    --+    -- An observant user might have also seen that a 'PasswordPolicy' includes a+    -- 'CharSetPredicate'. Very few users will want to change this from the+    -- 'defaultCharSetPredicate', since this includes all non-control ASCII characters.+    --+    -- If, for some reason, you'd like to accept more characters (e.g. é, ø, か, 事)+    -- or maybe you want to only allow alpha-numeric characters, 'charSetPredicate' is+    -- the place to do so.+    validatePasswordPolicy,+    validatePasswordPolicyTH,+    PasswordPolicy (..),+    ValidPasswordPolicy,+    fromValidPasswordPolicy,+    defaultPasswordPolicy,+    defaultPasswordPolicy_,+    CharSetPredicate(..),+    defaultCharSetPredicate,+    InvalidReason (..),+    InvalidPolicyReason(..),+    CharacterCategory(..),+    MinimumLength,+    MaximumLength,+    ProvidedLength,+    MinimumAmount,+    ProvidedAmount,+    -- * For internal use+    --+    -- | These are used in the test suite. You should not need these.+    --+    -- These are basically internal functions and as such have NO guarantee (__NONE__)+    -- to be consistent between releases.+    defaultCharSet,+    validateCharSetPredicate,+    categoryToPredicate,+    isSpecial,+    allButCSP+  ) where++import Data.Char (chr, isAsciiLower, isAsciiUpper, isDigit, ord)+import Data.Function (on)+import Data.List (foldl')++#if !MIN_VERSION_base(4,13,0)+import Data.Semigroup ((<>))+#endif+import Data.Text (Text)+import qualified Data.Text as T+import Language.Haskell.TH (Exp, Q, appE)+import Language.Haskell.TH.Syntax (Lift (..))++import Data.Password.Internal (Password (..))++-- $setup+-- >>> :set -XOverloadedStrings+--+-- Import needed libraries.+--+-- >>> import Data.Password++-- | Set of policies used to validate a 'Password'.+--+-- When defining your own 'PasswordPolicy', please keep in mind that:+--+-- * The value of 'maximumLength' must be bigger than 0+-- * The value of 'maximumLength' must be bigger than 'minimumLength'+-- * If any other field has a negative value (e.g. 'lowercaseChars'), it will be defaulted to 0+-- * The total sum of all character category values (i.e. all fields ending in @-Chars@)+--   must not be larger than the value of 'maximumLength'.+-- * The provided 'CharSetPredicate' needs to allow at least one of the characters in the+--   categories which require more than 0 characters. (e.g. if 'lowercaseChars' is > 0,+--   the 'charSetPredicate' must allow at least one of the characters in @[\'a\'..\'z\']@)+--+-- or else the validation functions will return one or more 'InvalidPolicyReason's.+--+-- If you're unsure of what to do, please use the default: 'defaultPasswordPolicy_'+--+-- @since 2.1.0.0+data PasswordPolicy = PasswordPolicy+    { minimumLength  :: !Int+    -- ^ Required password minimum length+    , maximumLength  :: !Int+    -- ^ Required password maximum length+    , uppercaseChars :: !Int+    -- ^ Required number of upper-case characters+    , lowercaseChars :: !Int+    -- ^ Required number of lower-case characters+    , specialChars   :: !Int+    -- ^ Required number of special characters+    , digitChars     :: !Int+    -- ^ Required number of ASCII-digit characters+    , charSetPredicate :: CharSetPredicate+    -- ^ Which characters are acceptable for use in passwords (cf. 'defaultCharSetPredicate')+    }++-- NB: KEEP THIS THE SAME ORDER AS THE PasswordPolicy FIELDS!+-- OTHERWISE THE 'validatePasswordPolicyTH' FUNCTION WILL BREAK.++-- @since 2.1.0.0+-- | All 'Int' fields of the 'PasswordPolicy' in a row+allButCSP :: PasswordPolicy -> [Int]+allButCSP PasswordPolicy{..} =+  [ minimumLength+  , maximumLength+  , uppercaseChars+  , lowercaseChars+  , specialChars+  , digitChars+  ]++-- | N.B. This will not check equality on the 'charSetPredicate'+instance Eq PasswordPolicy where+  (==) = go `on` allButCSP+    where+      go a b = and $ zipWith (==) a b++-- | N.B. This will not check order on the 'charSetPredicate'+instance Ord PasswordPolicy where+  compare = go `on` allButCSP+    where+      go a b = check $ zipWith compare a b+      check [] = EQ+      check (EQ : xs) = check xs+      check (x : _) = x++instance Show PasswordPolicy where+  show PasswordPolicy{..} = mconcat+    [ "PasswordPolicy {"+    , "minimumLength = ", show minimumLength+    , ", maximumLength = ", show maximumLength+    , ", uppercaseChars = ", show uppercaseChars+    , ", lowercaseChars = ", show lowercaseChars+    , ", specialChars = ", show specialChars+    , ", digitChars = ", show digitChars+    , ", charSetPredicate = <FUNCTION>}"+    ]++-- | A 'PasswordPolicy' that has been checked to be valid+--+-- @since 2.1.0.0+newtype ValidPasswordPolicy = VPP+  { fromValidPasswordPolicy :: PasswordPolicy+    -- ^+    -- In case you'd want to retrieve the 'PasswordPolicy'+    -- from the 'ValidPasswordPolicy'+    --+    -- @since 2.1.0.0+  } deriving (Eq, Ord, Show)++-- | Default value for the 'PasswordPolicy'.+--+-- Enforces that a password must be between 8-64 characters long and+-- have at least one uppercase letter, one lowercase letter and one digit,+-- though can easily be adjusted by using record update syntax:+--+-- @+-- myPolicy = defaultPasswordPolicy{ specialChars = 1 }+-- @+--+-- This policy on it's own is guaranteed to be valid. Any changes made to+-- it might result in 'validatePasswordPolicy' returning one or more+-- 'InvalidPolicyReason's.+--+-- >>> defaultPasswordPolicy+-- PasswordPolicy {minimumLength = 8, maximumLength = 64, uppercaseChars = 1, lowercaseChars = 1, specialChars = 0, digitChars = 1, charSetPredicate = <FUNCTION>}+--+-- @since 2.1.0.0+defaultPasswordPolicy :: PasswordPolicy+defaultPasswordPolicy = PasswordPolicy+  { minimumLength = 8,+    maximumLength = 64,+    uppercaseChars = 1,+    lowercaseChars = 1,+    specialChars = 0,+    digitChars = 1,+    charSetPredicate = defaultCharSetPredicate+  }++-- | Unchangeable 'defaultPasswordPolicy', but guaranteed to be valid.+--+-- @since 2.1.0.0+defaultPasswordPolicy_ :: ValidPasswordPolicy+defaultPasswordPolicy_ = VPP defaultPasswordPolicy++-- | Predicate which defines the characters that can be used for a password.+--+-- @since 2.1.0.0+newtype CharSetPredicate = CharSetPredicate+  { getCharSetPredicate :: Char -> Bool+  }++-- | The default character set consists of uppercase and lowercase letters, numbers,+-- and special characters from the @ASCII@ character set.+-- (i.e. everything from the @ASCII@ set except the control characters)+--+-- @since 2.1.0.0+defaultCharSetPredicate :: CharSetPredicate+defaultCharSetPredicate =  CharSetPredicate $ \c -> ord c >= 32 && ord c <= 126+{-# INLINE defaultCharSetPredicate #-}++-- @since 2.1.0.0+-- | Check if given 'Char' is a special character.+-- (i.e. any non-alphanumeric non-control ASCII character)+isSpecial :: Char -> Bool+isSpecial = \c ->+    isDefault c && not (isAsciiUpper c || isAsciiLower c || isDigit c)+  where+    CharSetPredicate isDefault = defaultCharSetPredicate++-- | Character categories+--+-- @since 2.1.0.0+data CharacterCategory+  = Uppercase+  -- ^ Uppercase letters+  | Lowercase+  -- ^ Lowercase letters+  | Special+  -- ^ Special characters+  | Digit+  -- ^ ASCII digits+  deriving (Eq, Ord, Show)++-- @since 2.1.0.0+-- | Convert a 'CharacterCategory' into its associated predicate function+categoryToPredicate :: CharacterCategory -> (Char -> Bool)+categoryToPredicate = \case+  Uppercase -> isAsciiUpper+  Lowercase -> isAsciiLower+  Special -> isSpecial+  Digit -> isDigit++type MinimumLength = Int+type MaximumLength = Int+type ProvidedLength = Int+type MinimumAmount = Int+type ProvidedAmount = Int++-- | Possible reasons for a 'Password' to be invalid.+--+-- @since 2.1.0.0+data InvalidReason+  = PasswordTooShort !MinimumLength !ProvidedLength+  -- ^ Length of 'Password' is too short.+  | PasswordTooLong !MaximumLength !ProvidedLength+  -- ^ Length of 'Password' is too long.+  | NotEnoughReqChars !CharacterCategory !MinimumAmount !ProvidedAmount+  -- ^ 'Password' does not contain required number of characters.+  | InvalidCharacters !Text+  -- ^ 'Password' contains characters that cannot be used+  deriving (Eq, Ord, Show)++-- | Possible reasons for a 'PasswordPolicy' to be invalid+--+-- @since 2.1.0.0+data InvalidPolicyReason+  = InvalidLength !MinimumLength !MaximumLength+  -- ^ Value of 'minimumLength' is bigger than 'maximumLength'+  --+  -- @InvalidLength minimumLength maximumLength@+  | MaxLengthBelowZero !MaximumLength+  -- ^ Value of 'maximumLength' is zero or less+  --+  -- @MaxLengthBelowZero maximumLength@+  | CategoryAmountsAboveMaxLength !MaximumLength !Int+  -- ^ The total of the character category amount requirements are+  -- higher than the maximum length of the password. (i.e. the 'Int' signifies+  -- the total of 'lowercaseChars' + 'uppercaseChars' + 'digitChars' + 'specialChars')+  --+  -- @CategoryAmountsAboveMaxLength maximumLength totalRequiredChars@+  | InvalidCharSetPredicate !CharacterCategory !MinimumAmount+  -- ^ 'charSetPredicate' does not return 'True' for a 'CharacterCategory' that+  -- requires at least 'MinimumAmount' characters in the password+  deriving (Eq, Ord, Show)++-- | Result of validating a 'Password'.+--+-- @since 2.1.0.0+data ValidationResult = ValidPassword | InvalidPassword [InvalidReason]+  deriving (Eq, Show)++-- | This function is equivalent to:+--+-- @'validatePassword' policy password == 'ValidPassword'@+--+-- >>> let pass = mkPassword "This_Is_Valid_PassWord1234"+-- >>> isValidPassword defaultPasswordPolicy_ pass+-- True+--+-- @since 2.1.0.0+isValidPassword :: ValidPasswordPolicy -> Password -> Bool+isValidPassword policy pass = validatePassword policy pass == ValidPassword+{-# INLINE isValidPassword #-}++-- | Checks if a given 'Password' adheres to the provided 'ValidPasswordPolicy'.+--+-- In case of an invalid password, returns the reasons why it wasn't valid.+--+-- >>> let pass = mkPassword "This_Is_Valid_Password1234"+-- >>> validatePassword defaultPasswordPolicy_ pass+-- ValidPassword+--+-- @since 2.1.0.0+validatePassword :: ValidPasswordPolicy -> Password -> ValidationResult+validatePassword (VPP PasswordPolicy{..}) (Password password) =+  case validationFailures of+    [] -> ValidPassword+    _:_ -> InvalidPassword validationFailures++  where+    validationFailures = mconcat+        [ isTooShort+        , isTooLong+        , isUsingValidCharacters+        , hasRequiredChar uppercaseChars Uppercase+        , hasRequiredChar lowercaseChars Lowercase+        , hasRequiredChar specialChars Special+        , hasRequiredChar digitChars Digit+        ]+    len = T.length password+    isTooLong = [PasswordTooLong maximumLength len | len > maximumLength]+    isTooShort = [PasswordTooShort minimumLength len | len < minimumLength]++    CharSetPredicate predicate = charSetPredicate+    isUsingValidCharacters :: [InvalidReason]+    isUsingValidCharacters =+        let filteredText = T.filter (not . predicate) password+        in [InvalidCharacters filteredText | not $ T.null filteredText]+    hasRequiredChar :: Int -> CharacterCategory -> [InvalidReason]+    hasRequiredChar requiredCharNum characterCategory+      | requiredCharNum <= 0 = []+      | otherwise =+          let p = categoryToPredicate characterCategory+              actualRequiredCharNum = T.length $ T.filter p password+          in [ NotEnoughReqChars characterCategory requiredCharNum actualRequiredCharNum+             | actualRequiredCharNum < requiredCharNum+             ]++-- | Validate 'CharSetPredicate' to return 'True' on at least one of the characters+-- that is required.+--+-- For instance, if 'PasswordPolicy' states that the password requires at least+-- one uppercase letter, then 'CharSetPredicate' should return True on at least+-- one uppercase letter.+validateCharSetPredicate :: PasswordPolicy -> [InvalidPolicyReason]+validateCharSetPredicate PasswordPolicy{..} =+  let charSets = accumulateCharSet+        [ (uppercaseChars, Uppercase)+        , (lowercaseChars, Lowercase)+        , (specialChars, Special)+        , (digitChars, Digit)+        ]+  in concatMap checkPredicate charSets+  where+    CharSetPredicate predicate = charSetPredicate+    checkPredicate :: (Int, CharacterCategory, String) -> [InvalidPolicyReason]+    checkPredicate (num, category, sets) =+      [InvalidCharSetPredicate category num | not $ any predicate sets]+    accumulateCharSet :: [(Int, CharacterCategory)] -> [(Int, CharacterCategory, String)]+    accumulateCharSet xs =+      [ (num, c, categoryToString c)+      | (num, c) <- xs+      , num > 0+      ]+    categoryToString :: CharacterCategory -> String+    categoryToString category = filter (categoryToPredicate category) defaultCharSet++-- | Template Haskell validation function for 'PasswordPolicy's.+--+-- @+-- {-\# LANGUAGE TemplateHaskell \#-}+-- myPolicy :: 'PasswordPolicy'+-- myPolicy = 'defaultPasswordPolicy'{ specialChars = 1 }+--+-- myValidPolicy :: 'ValidPasswordPolicy'+-- myValidPolicy = $('validatePasswordPolicyTH' myPolicy)+-- @+--+-- For technical reasons, the 'charSetPredicate' field is ignored and the+-- 'defaultCharSetPredicate' is used. If, for any reason, you do need to use a+-- custom 'CharSetPredicate', please use 'validatePasswordPolicy' and either handle+-- the failure case at runtime and/or use a unit test to make sure your policy is valid.+--+-- @since 2.1.0.0+validatePasswordPolicyTH :: PasswordPolicy -> Q Exp+validatePasswordPolicyTH pp =+    either (fail . showReasons) go $ validatePasswordPolicy withDefault+  where+    withDefault = pp{charSetPredicate = defaultCharSetPredicate}+    showReasons rs = "Bad password policy: " ++ show rs+    go _ = [|VPP|] `appE` newPP+    allButCSPQ = lift <$> allButCSP withDefault+    newPP = foldl' appE [|PasswordPolicy|] allButCSPQ `appE` [|defaultCharSetPredicate|]++-- | Verifies that a 'PasswordPolicy' is valid and converts it into a 'ValidPasswordPolicy'.+--+-- >>> validatePasswordPolicy defaultPasswordPolicy+-- Right (...)+--+-- @since 2.1.0.0+validatePasswordPolicy :: PasswordPolicy -> Either [InvalidPolicyReason] ValidPasswordPolicy+validatePasswordPolicy policy@PasswordPolicy{..} =+    case allReasons of+      [] -> Right $ VPP policy+      _ -> Left allReasons+  where+    allReasons = mconcat [validMaxLength, validLength, validCategoryAmount, validPredicate]+    validLength, validMaxLength, validCategoryAmount, validPredicate :: [InvalidPolicyReason]+    validLength =+        [InvalidLength minimumLength maximumLength | minimumLength > maximumLength]+    validMaxLength =+        [MaxLengthBelowZero maximumLength | maximumLength <= 0]+    validCategoryAmount =+        -- We don't report this reason if the maximumLength is already invalid+        [CategoryAmountsAboveMaxLength maximumLength total | total > maximumLength, maximumLength > 0]+      where+        capToZero = max 0+        total = sum $ capToZero <$> [lowercaseChars, uppercaseChars, digitChars, specialChars]+    validPredicate = validateCharSetPredicate policy++-- @since 2.1.0.0+-- | Default character set+--+-- Should be all non-control characters in the ASCII character set.+defaultCharSet :: String+defaultCharSet = chr <$> [32 .. 126]
test/tasty/Spec.hs view
@@ -8,6 +8,7 @@ import Bcrypt import PBKDF2 import Scrypt+import Validate  main :: IO () main = defaultMain $ localOption (NumThreads 1) $@@ -18,4 +19,5 @@     , testBcrypt     , testPBKDF2     , testScrypt+    , testValidate     ]
+ test/tasty/TestPolicy.hs view
@@ -0,0 +1,21 @@+module TestPolicy where++import Data.Password.Validate (+    PasswordPolicy (..),+    defaultPasswordPolicy,+ )++-- This is used for the TH test just so it would be obvious if+-- any numbers suddenly switched between fields.+-- (e.g. uppercaseChars and lowercaseChars suddenly switch places+-- after going through validatePasswordPolicyTH)+testPolicy :: PasswordPolicy+testPolicy =+    defaultPasswordPolicy+        { minimumLength = 8+        , maximumLength = 64+        , uppercaseChars = 3+        , lowercaseChars = 4+        , specialChars = 5+        , digitChars = 6+        }
+ test/tasty/Validate.hs view
@@ -0,0 +1,437 @@+{-# LANGUAGE CPP #-}+{-# LANGUAGE LambdaCase #-}+{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE RecordWildCards #-}+{-# LANGUAGE TemplateHaskell #-}+{-# OPTIONS_GHC -Wno-orphans #-}++module Validate where++import Control.Monad (replicateM)+import Data.Char (chr, isAsciiLower, isAsciiUpper, isControl, isDigit)+import Data.List (nub)+import Data.Text (Text)+import qualified Data.Text as T+import Test.QuickCheck.Instances.Text ()+import Test.Tasty (TestTree, testGroup)+import Test.Tasty.HUnit (Assertion, assertBool, assertEqual, testCase)+import Test.Tasty.QuickCheck (Arbitrary (..), Gen, Property, choose,+                              elements, oneof, shuffle, suchThat, testProperty,+                              withMaxSuccess, (===))+#if !MIN_VERSION_base(4,13,0)+import Data.Semigroup ((<>))+#endif++import Data.Password (Password, mkPassword)+import Data.Password.Validate hiding (ValidationResult(..))+import qualified Data.Password.Validate as V++import TestPolicy (testPolicy)+-- | Set of tests used for testing validate module+testValidate :: TestTree+testValidate =+  testGroup+    "Validation"+    [ testDefaultCharSetPredicate+    , testDefaultCharSet+    , testCase "defaultCharSetPredicate accepts all of defaultCharSet" $+        assertBool "defaultCharSet includes characters defaultCharSetPredicate disallows"+          $ all p defaultCharSet+    , testGroup "validatePasswordPolicy" validatePasswordPolicyTests++    , testProperty+        "Generator will always generate valid passwordPolicy"+        $ isRight . validatePasswordPolicy+    , testProperty "Valid password always true" prop_ValidPassword+    , testProperty "Generator will always generate valid FailedReason"+        (\(InvalidPassword reason _ _) -> either isValidPolicyReason isValidReason reason)+    , testProperty "validatePassword return appropriate value" prop_InvalidPassword+    ]+  where+    p = getCharSetPredicate defaultCharSetPredicate++isRight :: Either a b -> Bool+isRight = either (const False) (const True)++-- | Test that the defaultCharSetPredicate only allows non-control+-- characters from the ASCII character set.+testDefaultCharSetPredicate :: TestTree+testDefaultCharSetPredicate = testGroup "defaultCharSetPredicate"+    [ testCase "Return only non-control characters" $+        assertEqual+          "allows control characters"+          asciiNonControlChars+          $ filter p asciiCharSet+    , testCase "Return true on all non-control characters" $+        assertEqual+          "disallows valid characters"+          asciiControlChars+          $ filter (not . p) asciiCharSet+    ]+  where+    p = getCharSetPredicate defaultCharSetPredicate+    asciiCharSet, asciiControlChars, asciiNonControlChars :: [Char]+    asciiCharSet = chr <$> [0 .. 127]+    asciiControlChars = filter isControl asciiCharSet+    asciiNonControlChars = filter (not . isControl) asciiCharSet++testDefaultCharSet :: TestTree+testDefaultCharSet =+  testCase "defaultCharSet should contain all the expected characters" $ do+    hasCharacters isAsciiUpper ['A'..'Z'] "uppercase"+    hasCharacters isAsciiLower ['a'..'z'] "lowercase"+    hasCharacters isDigit ['0'..'9'] "digit"+    hasCharacters isSpecial specials "special"+  where+    specials = " !\"#$%&'()*+,-./:;<=>?@[\\]^_`{|}~"+    -- Check that the characters filtered from defaultCharSet is as expected+    hasCharacters :: (Char -> Bool) -> String -> String -> Assertion+    hasCharacters pre expected s =+      assertEqual msg expected $ filter pre defaultCharSet+      where+        msg = "Unexpected amount of " <> s <> " characters"++defaultPassword :: Password+defaultPassword = "Abcd3fgh"++validatePasswordPolicyTests :: [TestTree]+validatePasswordPolicyTests =+  [ testCase "defaultPasswordPolicy is valid" validDefaultPasswordPolicy+  , testCase "defaultCharSetPredicate is valid" validDefaultCharSetPredicate+  , testCase "defaultPassword is valid" validDefaultPassword+  , testProperty "upper case always valid" $+      validProp $ \i -> defaultPasswordPolicy{uppercaseChars = i}+  , testProperty "lower case always valid" $+      validProp $ \i -> defaultPasswordPolicy{lowercaseChars = i}+  , testProperty "digits always valid" $+      validProp $ \i -> defaultPasswordPolicy{digitChars = i}+  , testProperty "special chars always valid" $+      validProp $ \i -> defaultPasswordPolicy{specialChars = i}+  , testProperty "minimumLength always valid (maximumLength = maxBound)" $+      validProp $ \i ->+        defaultPasswordPolicy {+          minimumLength = i,+          maximumLength = maxBound+        }+  , testProperty "minimumLength <= maximumLength always valid" $+      \i -> let maxLen = maximumLength defaultPasswordPolicy+                mLength = [InvalidLength i maxLen | i > maxLen]+                policy = defaultPasswordPolicy { minimumLength = i }+                result = fromValidPasswordPolicy <$> validatePasswordPolicy policy+                expected = case mLength of+                  [] -> Right policy+                  _ -> Left mLength+            in result === expected+  , testProperty "maximumLength => always valid" $+      \i -> let minLen = minimumLength defaultPasswordPolicy+                mLength = [InvalidLength minLen i | i < minLen]+                mZero = [MaxLengthBelowZero i | i <= 0]+                mCategoryAmount = mkCategoryAmount policy+                reasons = mZero ++ mLength ++ mCategoryAmount+                policy = defaultPasswordPolicy { maximumLength = i }+                result = fromValidPasswordPolicy <$> validatePasswordPolicy policy+                expected = case reasons of+                  [] -> Right policy+                  _ -> Left reasons+            in result === expected+  , testGroup "TemplateHaskell" templateHaskellTest+  ]+  where+    mkCategoryAmount p =+        [ CategoryAmountsAboveMaxLength (maximumLength p) allCategoryAmounts+        | allCategoryAmounts > maxLength+        , maxLength > 0+        ]+      where+        maxLength = maximumLength p+        allCategoryAmounts = sum $ max 0 <$> [lowercaseChars p, uppercaseChars p, digitChars p, specialChars p]+    validProp f i =+      let p = f i+          result = fromValidPasswordPolicy <$> validatePasswordPolicy p+          reason = mkCategoryAmount p+          expected = case reason of+            [] -> Right p+            _ -> Left reason+      in result === expected++validDefaultPasswordPolicy :: Assertion+validDefaultPasswordPolicy = do+  assertBool "defaultPasswordPolicy isn't a valid policy"+    $ isRight $ validatePasswordPolicy defaultPasswordPolicy+  assertBool "defaultPasswordPolicy_ isn't a valid policy"+    $ Right defaultPasswordPolicy_ == validatePasswordPolicy (fromValidPasswordPolicy defaultPasswordPolicy_)++templateHaskellTest :: [TestTree]+templateHaskellTest =+    [ testCase "allButCSP check" $ assertBool+        "testPolicy doesn't have unique values"+        $ let ns = V.allButCSP testPolicy+          in nub ns == ns+    , testCase "Internal consistency" $ assertEqual+        "validatePasswordPolicyTH switched values between fields"+        testPolicy+        $ fromValidPasswordPolicy $(validatePasswordPolicyTH testPolicy)+    , testCase "defaultPassword" $ assertBool+        "valid policy from TH can't parse defaultPassword"+        $ isValidPassword $(validatePasswordPolicyTH defaultPasswordPolicy) defaultPassword+    , testCase "TH ignores charSetPredicate (always uses default)" $ assertBool+        "TH still parses defaultPassword when pred is initially bad"+        $ isValidPassword+            $(validatePasswordPolicyTH defaultPasswordPolicy{ charSetPredicate = CharSetPredicate $ const False })+            defaultPassword+    , testProperty "prop test for charSetPredicate after TH" $ \(PredInts i) ->+        let policy = $(validatePasswordPolicyTH defaultPasswordPolicy{ charSetPredicate = CharSetPredicate $ const False})+            pred = getCharSetPredicate . charSetPredicate $ fromValidPasswordPolicy policy+            c = chr i+        in pred c == (c `elem` defaultCharSet)+    ]++newtype PredInts = PredInts { unPredInts :: Int } deriving (Eq, Show)++instance Arbitrary PredInts where+    arbitrary = PredInts <$> choose (0, 260)++validDefaultCharSetPredicate :: Assertion+validDefaultCharSetPredicate =+  assertEqual "defaultCharSetPredicate isn't valid" []+    $ validateCharSetPredicate defaultPasswordPolicy++validDefaultPassword :: Assertion+validDefaultPassword =+  assertEqual "defaultPassword isn't valid" V.ValidPassword+    $ validatePassword defaultPasswordPolicy_ defaultPassword++--------------------------------------------------------------------------------+-- Typeclass instances+--------------------------------------------------------------------------------++-- | Generate valid PasswordPolicy+instance Arbitrary PasswordPolicy where+  arbitrary = do+    minimumLength <- genCharLength+    uppercaseChars <- genCharLength+    lowercaseChars <- genCharLength+    specialChars <- genCharLength+    digitChars <- genCharLength+    let sumLength = sum [uppercaseChars, lowercaseChars, specialChars, digitChars]+    let minMaxLength = max minimumLength sumLength+    maximumLength <- choose (minMaxLength, minMaxLength + 10)+    let charSetPredicate = defaultCharSetPredicate+    return PasswordPolicy{..}+    where+      genCharLength :: Gen Int+      genCharLength = choose (1, 10)++instance Arbitrary CharacterCategory where+  arbitrary = elements [Uppercase, Lowercase, Special, Digit]++-- This is needed for testing (QuickCheck requires given datatype to have Show instance)+instance Show CharSetPredicate where+  show _ = "Predicate"++--------------------------------------------------------------------------------+-- Tests+--------------------------------------------------------------------------------++-- | Test that 'validatePassword' will always return empty list if the password+-- is valid+prop_ValidPassword :: ValidPassword -> Property+prop_ValidPassword (ValidPassword passwordPolicy password) =+  withMaxSuccess 1000 $+    case validatePasswordPolicy passwordPolicy of+      Left _ -> error "PasswordPolicy is invalid"+      Right validPolicy -> validatePassword validPolicy (mkPassword password) === V.ValidPassword++-- | Data type used to generate valid password and 'PasswordPolicy' associated+-- with it+data ValidPassword = ValidPassword+  { validPasswordPolicy   :: !PasswordPolicy+  , validPasswordText     :: !Text+  } deriving (Show)++instance Arbitrary ValidPassword where+  arbitrary = do+    policy@PasswordPolicy{..} <- arbitrary+    passLength <- choose (minimumLength, maximumLength)+    passwordText <- genPassword passLength policy+    return $ ValidPassword policy passwordText+    where+      genPassword :: Int -> PasswordPolicy -> Gen Text+      genPassword passLength PasswordPolicy{..} = do+        upperCase <- genStr uppercaseChars isAsciiUpper+        lowerCase <- genStr lowercaseChars isAsciiLower+        specialChar <- genStr specialChars isSpecial+        digit <- genStr digitChars isDigit+        let requiredChars = upperCase <> lowerCase <> specialChar <> digit+        let toFill = passLength - length requiredChars+        fillChars <- replicateM toFill $+            arbitrary `suchThat` getCharSetPredicate charSetPredicate+        T.pack <$> shuffle (fillChars <> requiredChars)+      genStr :: Int -> (Char -> Bool) -> Gen String+      genStr num predicate = replicateM num (arbitrary `suchThat` predicate)++prop_InvalidPassword :: InvalidPassword -> Property+prop_InvalidPassword (InvalidPassword failedReason passwordPolicy password) =+  withMaxSuccess 1000 $ (===) expected $+    case validatePasswordPolicy passwordPolicy of+      Left policyReasons -> Left policyReasons+      Right validPolicy -> Right $ validatePassword validPolicy (mkPassword password)+  where+    expected = either (Left . pure) (Right . V.InvalidPassword . pure) failedReason++-- | Data type used to generate password which does not follow one of the policies+-- as well as 'InvalidReason', 'CharSetPredicate', and 'PasswordPolicy' associated with it+data InvalidPassword = InvalidPassword+  { invalidPassFailedReason :: !(Either InvalidPolicyReason InvalidReason)+  , invalidPassPolicy       :: !PasswordPolicy+  , invalidPasswordText     :: !Text+  } deriving (Show)++instance Arbitrary InvalidPassword where+  arbitrary = do+    reason <- genFailedReason emptyPolicy+    let charSetP = updateCharSetPredicate defaultCharSetPredicate reason+        updatedPolicy = (updatePolicy emptyPolicy reason){ charSetPredicate = charSetP }+    passText <- genInvalidPassword updatedPolicy reason+    return $ InvalidPassword reason updatedPolicy (T.pack passText)+    where+      genFailedReason :: PasswordPolicy -> Gen (Either InvalidPolicyReason InvalidReason)+      genFailedReason policy@PasswordPolicy{..} =+        oneof+          [ Right <$> genTooShort maximumLength,+            Right <$> genTooLong minimumLength,+            Right <$> genNotEnoughRequiredChars,+            Right <$> genInvalidChar defaultCharSetPredicate,+            Left <$> oneof+              [ MaxLengthBelowZero <$> (arbitrary `suchThat` (<= 0))+              , genInvalidLength policy+              , genInvalidCategoryAmountTotal policy+              , InvalidCharSetPredicate <$> arbitrary <*> choose (minimumLength, maximumLength)+              ]+          ]+      genTooShort :: Int -> Gen InvalidReason+      genTooShort maxLength = do+        requiredLength <- choose (1, maxLength - 1)+        actualLength <- choose (0, requiredLength - 1)+        return $ PasswordTooShort requiredLength actualLength+      genTooLong :: Int -> Gen InvalidReason+      genTooLong minLength = do+        requiredLength <- choose (minLength + 1, 30)+        actualLength <- choose (requiredLength + 1, 50)+        return $ PasswordTooLong requiredLength actualLength+      genNotEnoughRequiredChars :: Gen InvalidReason+      genNotEnoughRequiredChars = do+        required <- choose (1, 3)+        actual <- choose (0, required - 1)+        category <- arbitrary+        return $ NotEnoughReqChars category required actual+      genInvalidChar :: CharSetPredicate -> Gen InvalidReason+      genInvalidChar (CharSetPredicate predicate) = do+        num <- choose (1, 3)+        let arbitraryInvalidChar = arbitrary `suchThat` (not . predicate)+        chrs <- replicateM num arbitraryInvalidChar+        return $ InvalidCharacters (T.pack chrs)+      genInvalidLength :: PasswordPolicy -> Gen InvalidPolicyReason+      genInvalidLength PasswordPolicy{..} = do+        let sumReq = sum [uppercaseChars, lowercaseChars, specialChars, digitChars]+        minLength <- choose (sumReq, maximumLength - 1) `suchThat` (> 0)+        return $ InvalidLength maximumLength minLength+      genInvalidCategoryAmountTotal :: PasswordPolicy -> Gen InvalidPolicyReason+      genInvalidCategoryAmountTotal p = do+          maxLength <- arbitrary `suchThat` (> minimumLength p)+          let totalAmount = maxLength + 1+          return $ CategoryAmountsAboveMaxLength maxLength totalAmount+      -- Update 'PasswordPolicy' based upon 'InvalidReason'+      updatePolicy :: PasswordPolicy -> Either InvalidPolicyReason InvalidReason -> PasswordPolicy+      updatePolicy policy = \case+        Right (PasswordTooShort req _actual) -> policy {minimumLength = req}+        Right (PasswordTooLong req _actual) -> policy {maximumLength = req}+        Right (InvalidCharacters _invalidChars) -> policy+        Right (NotEnoughReqChars category req _actual) ->+          case category of+            Uppercase -> policy {uppercaseChars = req}+            Lowercase -> policy {lowercaseChars = req}+            Special   -> policy {specialChars = req}+            Digit     -> policy {digitChars = req}+        Left (MaxLengthBelowZero num) ->+          policy {+            minimumLength = num - 1,+            maximumLength = num+          }+        Left (InvalidLength minLength maxLength) ->+          policy {+            minimumLength = minLength,+            maximumLength = maxLength+          }+        Left (CategoryAmountsAboveMaxLength maxLength totalAmount) ->+          policy {+              maximumLength = maxLength,+              lowercaseChars = totalAmount+          }+        Left (InvalidCharSetPredicate category num) ->+          case category of+            Uppercase -> policy {uppercaseChars = num}+            Lowercase -> policy {lowercaseChars = num}+            Special   -> policy {specialChars = num}+            Digit     -> policy {digitChars = num}+      updateCharSetPredicate :: CharSetPredicate -> Either InvalidPolicyReason InvalidReason -> CharSetPredicate+      updateCharSetPredicate predicate = \case+        Right (InvalidCharacters invalidChars) ->+          CharSetPredicate $ \c -> getCharSetPredicate predicate c && c `notElem` T.unpack invalidChars+        Left (InvalidCharSetPredicate category _num) ->+          let filterPre = categoryToPredicate category+          in CharSetPredicate $ \c -> getCharSetPredicate predicate c && (not . filterPre) c+        _others -> predicate+      genInvalidPassword :: PasswordPolicy -> Either InvalidPolicyReason InvalidReason -> Gen String+      genInvalidPassword policy@PasswordPolicy{..} = \case+        Right (PasswordTooShort _req actual) -> genPassword actual charSetPredicate+        Right (PasswordTooLong _req actual) -> genPassword actual charSetPredicate+        Right (NotEnoughReqChars category _req actual) -> do+          passwordLength <- genPasswordLength policy+          let pre = categoryToPredicate category+          let usableCharsets = CharSetPredicate $ \c -> not (pre c) && getCharSetPredicate charSetPredicate c+          requiredChars <- replicateM actual (arbitrary `suchThat` pre)+          passwordText <- genPassword (passwordLength - actual) usableCharsets+          shuffle (passwordText <> requiredChars)+        Right (InvalidCharacters chrs) -> do+          passwordLength <- genPasswordLength policy+          passwordText <- genPassword (passwordLength - T.length chrs) charSetPredicate+          -- Here, we make sure that the order of invalid characters are apporiate+          -- or else the test will fail. For instance this will make the test fail+          -- since the order of the characters are different+          -- e.g. [InvalidChar "一二三"] /= [InvalidChar "三二一"]+          let s = T.unpack chrs+          shuffle (passwordText <> s) `suchThat` checkOrder charSetPredicate s+        Left (MaxLengthBelowZero _invalid) -> genPassword minimumLength charSetPredicate+        _others -> do+          passwordLength <- genPasswordLength policy+          genPassword passwordLength charSetPredicate+      genPassword :: Int -> CharSetPredicate -> Gen String+      genPassword num (CharSetPredicate predicate) = replicateM num (arbitrary `suchThat` predicate)+      genPasswordLength :: PasswordPolicy -> Gen Int+      genPasswordLength PasswordPolicy{..} = choose (minimumLength, maximumLength)+      checkOrder :: CharSetPredicate -> String -> String -> Bool+      checkOrder (CharSetPredicate predicate) invalidChars password =+        let filteredChars = filter (not . predicate) password+         in invalidChars == filteredChars++-- | Check if given 'InvalidReason' is valid+isValidReason :: InvalidReason -> Bool+isValidReason = \case+  InvalidCharacters _ -> True+  PasswordTooLong required actual -> required < actual+  PasswordTooShort required actual -> required > actual+  NotEnoughReqChars _ required actual -> required > actual++isValidPolicyReason :: InvalidPolicyReason -> Bool+isValidPolicyReason = \case+  InvalidLength minLength maxLength -> minLength > maxLength+  MaxLengthBelowZero num -> num <= 0+  CategoryAmountsAboveMaxLength maxLength num -> num > maxLength+  InvalidCharSetPredicate _category num -> num > 0++-- | 'PasswordPolicy' used for testing+--+-- Required characters are turned off so that it's much more easier to test.+emptyPolicy :: PasswordPolicy+emptyPolicy = PasswordPolicy 8 32 0 0 0 0 defaultCharSetPredicate