elocrypt 2.0.1 → 2.1.0
raw patch · 11 files changed
+295/−281 lines, 11 filesdep +containersPVP ok
version bump matches the API change (PVP)
Dependencies added: containers
API changes (from Hackage documentation)
- Data.Elocrypt: newtype GenOptions
+ Data.Elocrypt: [genDigits] :: GenOptions -> Bool
+ Data.Elocrypt: [genSpecials] :: GenOptions -> Bool
+ Data.Elocrypt: data GenOptions
+ Data.Elocrypt: numerize1 :: MonadRandom m => Int -> String -> m String
+ Data.Elocrypt: numerizeR :: MonadRandom m => Int -> String -> m String
+ Data.Elocrypt: specialize1 :: MonadRandom m => Int -> String -> m String
+ Data.Elocrypt: specializeR :: MonadRandom m => Int -> String -> m String
+ Data.Elocrypt.Utils: isSymbol :: Char -> Bool
+ Data.Elocrypt.Utils: numeralConversions :: Map Char [Char]
+ Data.Elocrypt.Utils: symbolConversions :: Map Char [Char]
+ Data.Elocrypt.Utils: toDigit :: Char -> String
+ Data.Elocrypt.Utils: toSymbol :: Char -> String
+ Data.Elocrypt.Utils: update1 :: Monad m => (Char -> m Char) -> String -> Int -> m String
+ Data.Elocrypt.Utils: updateR :: MonadRandom m => (Char -> m Char) -> Rational -> String -> m String
- Data.Elocrypt: GenOptions :: Bool -> GenOptions
+ Data.Elocrypt: GenOptions :: Bool -> Bool -> Bool -> GenOptions
Files
- README.md +6/−6
- elocrypt.cabal +6/−21
- src/cli/Main.hs +21/−3
- src/lib/Data/Elocrypt.hs +53/−16
- src/lib/Data/Elocrypt/Utils.hs +63/−0
- test/IntegTest/Elocrypt/PassphraseTest.hs +0/−121
- test/IntegTest/Elocrypt/PasswordTest.hs +0/−98
- test/Test/Elocrypt/QuickCheck.hs +24/−12
- test/Test/Elocrypt/UtilsTest.hs +90/−0
- test/Test/ElocryptTest.hs +29/−3
- test/Tests.hs +3/−1
README.md view
@@ -5,8 +5,8 @@ ## Prerequisites In order to build or install you will need- * GHC (tested on 8.0)- * cabal-install (tested on 1.24)+ * GHC (tested on 8.6.5)+ * Stack (tested on 2.1.3.1) ## Installing Elocrypt is on [Hackage](https://hackage.haskell.org/package/elocrypt). Installation is as easy as:@@ -15,8 +15,8 @@ ``` Binaries are also available:- * [elocrypt-2.0.0-linux-bin.tar.gz](https://github.com/sgillespie/elocrypt/releases/download/v2.0.0/elocrypt-2.0.0-linux-bin.tar.gz)- * [elocrypt-2.0.0-windows-bin.exe](https://github.com/sgillespie/elocrypt/releases/download/v2.0.0/elocrypt-2.0.0-windows-bin.exe)+ * [elocrypt-2.1.0-linux-bin.tar.gz](https://github.com/sgillespie/elocrypt/releases/download/v2.1.0/elocrypt-v2.1.0-linux-bin.tar.gz)+ * [elocrypt-2.1.0-osx-bin.exe](https://github.com/sgillespie/elocrypt/releases/download/v2.1.0/elocrypt-v2.1.0-osx-bin.tar.gz) ## Running Generate a list passwords:@@ -36,8 +36,8 @@ ## Building In order to build or install you will need- * [GHC](https://www.haskell.org/ghc) (tested on 8.0)- * [Haskell Stack](https://haskellstack.org) (tested on 1.24)+ * [GHC](https://www.haskell.org/ghc) (tested on 8.6.5)+ * [Haskell Stack](https://haskellstack.org) (tested on 2.1.3.1) Build elocrypt: ```
elocrypt.cabal view
@@ -1,5 +1,5 @@ name: elocrypt-version: 2.0.1+version: 2.1.0 synopsis: Generate easy-to-remember, hard-to-guess passwords homepage: https://www.github.com/sgillespie/elocrypt license: OtherLicense@@ -30,11 +30,13 @@ library exposed-modules: Data.Elocrypt,- Data.Elocrypt.Trigraph+ Data.Elocrypt.Trigraph,+ Data.Elocrypt.Utils -- other-modules: -- other-extensions: build-depends: base >=4.7 && <5, MonadRandom,+ containers, random hs-source-dirs: src/lib default-language: Haskell2010@@ -53,6 +55,7 @@ MonadRandom, proctest, QuickCheck,+ containers, random, tasty, tasty-quickcheck,@@ -63,26 +66,8 @@ other-modules: Test.Elocrypt.Instances, Test.Elocrypt.QuickCheck, Test.Elocrypt.TrigraphTest,+ Test.Elocrypt.UtilsTest, Test.ElocryptTest- type: exitcode-stdio-1.0--test-suite ui-test- build-depends: base >= 4.7 && <5,- elocrypt,- proctest,- MonadRandom,- QuickCheck,- random,- tasty,- tasty-quickcheck,- tasty-th >= 0.1.7- default-language: Haskell2010- hs-source-dirs: test- main-is: IntegTests.hs- other-modules: IntegTest.Elocrypt.PassphraseTest,- IntegTest.Elocrypt.PasswordTest,- Test.Elocrypt.QuickCheck,- Test.Elocrypt.Instances type: exitcode-stdio-1.0 test-suite elocrypt-lint
src/cli/Main.hs view
@@ -16,16 +16,18 @@ termLen :: Int termHeight :: Int -version = "elocrypt 2.0.1"+version = "elocrypt 2.1.0" termLen = 80 termHeight = 10 data Options = Options { optCapitals :: Bool, -- Include capital letters?+ optDigits :: Bool, -- Inlcude digits? optLength :: Int, -- Size of the password(s) optMaxLength :: Int, optNumber :: Maybe Int, -- Number of passwords to generate optPassType :: PassType, -- Generate passwords or passphrases+ optSpecials :: Bool, -- Include special characters? optHelp :: Bool, optVersion :: Bool } deriving (Show)@@ -38,10 +40,12 @@ defaultOptions :: Options defaultOptions = Options { optCapitals = False+ , optDigits = False , optLength = 8 , optMaxLength = 10 , optNumber = Nothing , optPassType = Word+ , optSpecials = False , optHelp = False , optVersion = False }@@ -54,6 +58,16 @@ (NoArg (\o -> o { optCapitals = True })) "Include at least one capital letter" , Option+ ['d']+ ["digits"]+ (NoArg (\o -> o { optDigits = True }))+ "Include numerals"+ , Option+ ['s']+ ["symbols"]+ (NoArg (\o -> o { optSpecials = True }))+ "Include special characters"+ , Option ['n'] ["number"] (ReqArg (\n o -> o { optNumber = Just (read n) }) "NUMBER")@@ -132,8 +146,12 @@ lines' = fromMaybe termHeight n getGenOptions :: Options -> GenOptions-getGenOptions Options { optCapitals = caps } =- genOptions { genCapitals = caps }+getGenOptions opts+ = genOptions+ { genCapitals = optCapitals opts,+ genDigits = optDigits opts,+ genSpecials = optSpecials opts+ } usage :: String usage = usageInfo (intercalate "\n" headerLines) options
src/lib/Data/Elocrypt.hs view
@@ -11,27 +11,37 @@ 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.-newtype GenOptions = GenOptions {- genCapitals :: Bool+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}+genOptions = GenOptions {+ genCapitals = False,+ genDigits = False,+ genSpecials = False+} -- * Random password generators @@ -120,8 +130,15 @@ pass <- if len > 2 then lastN (len - 2) f2' else return (take len f2') let pass' = reverse pass - if genCapitals opts then capitalizeR len pass' else return 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. --@@ -220,17 +237,37 @@ -- |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 = mapM capitalize s >>= capitalize1 len- where capitalize ch = fromList [(ch, 12), (toUpper ch, 1)]+capitalizeR len s = capitalize s >>= capitalize1 len+ where capitalize = updateR (return . toUpper) (1 % 12) -- |Randomly capitalize 1 character-capitalize1- :: MonadRandom m- => Int -- ^ length- -> String -- ^ the string to capitalize- -> m String-capitalize1 len s = capitalize1' <$> getRandomR (0, len - 1)- where- capitalize1' pos =- let (prefix, ch : suffix) = splitAt (pos - 1) s- in prefix ++ (toUpper ch : suffix)+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)
+ src/lib/Data/Elocrypt/Utils.hs view
@@ -0,0 +1,63 @@+module Data.Elocrypt.Utils where++import Data.Char (isAlphaNum, isSpace)+import Data.Maybe (fromMaybe)+import Data.Ratio+import qualified Data.Map as M++import Control.Monad.Random (MonadRandom(), fromList)++-- |A mapping from letters to numbers that look like them+numeralConversions = M.fromList [+ ('o', ['0']),+ ('l', ['1']),+ ('z', ['2']),+ ('e', ['3']),+ ('a', ['4']),+ ('s', ['5']),+ ('g', ['6', '9']),+ ('t', ['7']),+ ('b', ['8'])]++-- |A mapping from letters to symbols that look like them+symbolConversions = M.fromList [+ ('a', ['@']),+ ('l', ['!']),+ ('s', ['$'])]++-- |Map a letter to one or more digits, if possible+toDigit :: Char -> String+toDigit c = fromMaybe [c] (numeralConversions M.!? c)++-- |Map a letter to one or more symbols, if possible+toSymbol :: Char -> String+toSymbol c = fromMaybe [c] (symbolConversions M.!? c)++-- |Selects special characters+isSymbol :: Char -> Bool+isSymbol c = not (isAlphaNum c || isSpace c)++-- |Randomly update characters at the specified probability+updateR+ :: MonadRandom m+ => (Char -> m Char)+ -> Rational+ -> String+ -> m String+updateR f prob = mapM f'+ where f' ch = do+ ch' <- f ch+ fromList [+ (ch, toRational $ denominator prob),+ (ch', toRational $ numerator prob)]++-- |Update character at position pos+update1+ :: Monad m+ => (Char -> m Char) -- ^ Update function+ -> String -- ^ the string to update+ -> Int -- ^ the position to update+ -> m String+update1 _ "" _ = return ""+update1 f s pos = (\ch' -> prefix ++ ch' : suffix) <$> f ch+ where (prefix, ch : suffix) = splitAt pos s
− test/IntegTest/Elocrypt/PassphraseTest.hs
@@ -1,121 +0,0 @@-{-# LANGUAGE TemplateHaskell #-}-module IntegTest.Elocrypt.PassphraseTest where--import Control.Monad-import Data.Bool-import Data.Char-import Data.List-import Data.Maybe--import Test.QuickCheck-import Test.Tasty hiding (Timeout)-import Test.Tasty.QuickCheck (testProperty)-import Test.Tasty.TH-import qualified Test.Proctest as Proctest--import Test.Elocrypt.QuickCheck--tests :: TestTree-tests = $(testGroupGenerator)--elocrypt = "elocrypt"---- |Passphrases consist of words in specified length range-prop_printsWordsWithLengthRange :: PhraseCliOptions -> Property-prop_printsWordsWithLengthRange (PhraseCliOptions opts)- = isJust minLen && isJust maxLen && fromJust maxLen <= 79 ==>- ioProperty $ do- (_, out, _, _) <- run opts- response <- readHandle out-- let words' = words response- minLen' = fromJust minLen- maxLen' = fromJust maxLen-- return (all ((\n -> n >= minLen' && n <= maxLen') . length) words')-- where CliOptions{cliLength=minLen, cliMaxLength=maxLen} = opts---- |Passphrases consist of words with specified minimum range-prop_printsWordsWithMinLength:: PhraseCliOptions -> Property-prop_printsWordsWithMinLength(PhraseCliOptions opts)- = isJust minLen && fromJust minLen <= 79 ==>- ioProperty $ do- (_, out, _, _) <- run opts- response <- readHandle out-- return . all ((>= min (fromJust minLen) 10) . length) . words $ response-- where CliOptions{cliLength=minLen} = opts---- |Prints the specificed number of phrases-prop_printsSpecifiedNumberOfPassphrases :: PhraseCliOptions -> Property-prop_printsSpecifiedNumberOfPassphrases (PhraseCliOptions opts)- = isJust number && - (isJust maxLen || isNothing len) && -- maxLen > len- maybe True (< 79) maxLen ==> -- minLen/maxLen < 79-- ioProperty $ do-- (_, out, _, _) <- run opts- response <- readHandle out-- let phrases = lines response-- return $ counterexample (failMsg phrases) - (fromJust number == length phrases)-- where CliOptions{cliNumber=number, cliLength=len, cliMaxLength=maxLen} = opts- failMsg p = "length phrases (" ++ show (length p) ++ - ") /= " ++ show (fromJust number)---- |Prints multiple passwords per line-prop_printsMultipleWordsPerLine :: PhraseCliOptions - -> LessThan20 Int- -> LessThan20 Int- -> Property-prop_printsMultipleWordsPerLine (PhraseCliOptions opts) (LT20 min) (LT20 max)- = ioProperty $ do- let opts' = opts { -- Make sure we can fit 2+ words on a line- cliLength = Just min, - cliMaxLength = Just max}-- (_, out, _, _) <- run opts'- response <- readHandle out-- return $- all (>1) . tail . reverse . map (length . words) . lines $ response---- |Always prints a passphrase-prop_printsLongPassphrases :: PhraseCliOptions- -> Positive Int- -> Positive Int - -> Property-prop_printsLongPassphrases (PhraseCliOptions opts) (Positive min) (Positive max)- = forAll (scale (*7) arbitrary) $ \min ->- ioProperty $ do- let minLen = getPositive min- opts' = opts{- cliLength = Just minLen,- cliMaxLength = Just (minLen + max)}-- (_, out, _, _) <- run opts- response <- readHandle out- - return $- cover 20 (minLen > 80) "long" $ - all (>=1) . map (length . words) . lines $ response---- |Prints capitals when specified-prop_printsCapitals :: PhraseCliOptions -> Property-prop_printsCapitals (PhraseCliOptions opts)- = ioProperty $ do- let opts' = opts { cliCapitals = True }-- (_, out, _, _) <- run opts'- response <- readHandle out-- let phrases = lines response-- return $- cover 80 (any (any isUpper) phrases) "has caps" True
− test/IntegTest/Elocrypt/PasswordTest.hs
@@ -1,98 +0,0 @@-{-# LANGUAGE TemplateHaskell #-}-module IntegTest.Elocrypt.PasswordTest where--import Data.Char-import Data.List-import Data.Maybe-import Control.Monad--import Test.Tasty hiding (Timeout)-import Test.Tasty.QuickCheck (testProperty)-import Test.Tasty.TH--import Test.Elocrypt.QuickCheck--tests :: TestTree-tests = $(testGroupGenerator)--elocrypt = "elocrypt"---- |All passwords have specified length-prop_printsPasswordsWithLength :: WordCliOptions -> Property-prop_printsPasswordsWithLength (WordCliOptions opts)- = isJust len ==>- ioProperty $ do- (_, out, _, _) <- run opts- response <- readHandle out-- let len' = fromJust len- words' = words response-- return (all ((==) len' . length) words')-- where CliOptions{cliLength=len} = opts---- |Prints nothing when length is 0-prop_printsNothingWhenLengthIsZero :: WordCliOptions -> Property-prop_printsNothingWhenLengthIsZero (WordCliOptions opts)- = ioProperty $ do- let opts' = opts { cliLength = Just 0 }-- (_, out, _, _) <- run opts'- response <- readHandle out- return (response == "")---- |Always prints at least 1 password-prop_printsLongPasswords :: WordCliOptions -> Property-prop_printsLongPasswords (WordCliOptions opts)- = forAll (scale (*7) arbitrary) $ \len ->- ioProperty $ do- let len' = getPositive len- opts' = opts { cliLength = Just len' }-- (_, out, _, _) <- run opts'- response <- readHandle out-- return $- cover 30 (len' > 80) "long" $- all (>=1) . map (length . words) . lines $ response---- |Prints the specified number of passwords-prop_printsNumberPasswords :: Positive Int -> WordCliOptions -> Property-prop_printsNumberPasswords (Positive num) (WordCliOptions opts)- = ioProperty $ do- let opts' = opts { cliNumber = Just num }-- (_, out, _, _) <- run opts'- response <- readHandle out-- let words' = words response-- return $ num == length words'---- |Prints multiple passwords per line when length is sufficiently small-prop_printsMultiplePasswordsPerLine :: WordCliOptions -> Property-prop_printsMultiplePasswordsPerLine (WordCliOptions opts)- = isNothing len || fromJust len <= 38 ==>- ioProperty $ do- (_, out, _, _) <- run opts- response <- readHandle out-- return $- all (>1) . tail . reverse . map (length . words) . lines $ response-- where CliOptions{cliLength=len} = opts---- |Prints capitals when specified-prop_printsCapitals :: WordCliOptions -> Property-prop_printsCapitals (WordCliOptions opts)- = ioProperty $ do- let opts' = opts { cliCapitals = True }-- (_, out, _, _) <- run opts'- response <- readHandle out-- let passes = words response-- return $- cover 80 (any (any isUpper) passes) "has caps" True
test/Test/Elocrypt/QuickCheck.hs view
@@ -32,10 +32,12 @@ -- options data CliOptions = CliOptions { cliCapitals :: Bool,+ cliDigits :: Bool, cliLength :: Maybe Int, cliMaxLength :: Maybe Int, cliNumber :: Maybe Int,- cliPassphrase :: Bool+ cliPassphrase :: Bool,+ cliSpecials :: Bool } deriving Eq instance Show CliOptions where@@ -51,16 +53,20 @@ instance Arbitrary WordCliOptions where arbitrary = do- caps <- arbitrary- len <- arbitrary- num <- arbitrary+ caps <- arbitrary+ digits <- arbitrary+ len <- arbitrary+ num <- arbitrary+ specials <- arbitrary return $ WordCliOptions CliOptions{ cliCapitals = caps,+ cliDigits = digits, cliLength = fromPositive len, cliMaxLength = Nothing, cliNumber = fromPositive num,- cliPassphrase = False+ cliPassphrase = False,+ cliSpecials = specials } where fromPositive = fmap getPositive@@ -75,10 +81,12 @@ instance Arbitrary PhraseCliOptions where arbitrary = do- caps <- arbitrary- minLen <- arbitrary- maxLen <- arbitrary- num <- arbitrary+ caps <- arbitrary+ digits <- arbitrary+ minLen <- arbitrary+ maxLen <- arbitrary+ num <- arbitrary+ specials <- arbitrary -- CLI gets very inconsistent when len >= 80 -- TODO: Fix ^^^@@ -87,12 +95,14 @@ return $ PhraseCliOptions CliOptions{ cliCapitals = caps,+ cliDigits = digits, cliLength = minLen', cliMaxLength = maxLen', -- CLI gets very inconsistent when number >= 20 -- TODO: Fix ^^^ cliNumber = fromLT20 num,- cliPassphrase = True+ cliPassphrase = True,+ cliSpecials = specials } newtype LessThan20 n = LT20 { getLT20 :: n } deriving (Eq, Show)@@ -111,13 +121,15 @@ -- |Convert CliOptions into a list of cli arguments getOptions :: CliOptions -> [String] getOptions opts- = caps ++ num ++ phrase ++ len ++ maxLen+ = caps ++ digits ++ specials ++ num ++ phrase ++ len ++ maxLen where caps = ["-c" | cliCapitals opts]+ digits = ["-d" | cliDigits opts] len = maybe [] (singleton . show) (cliLength opts) maxLen = maybe [] (singleton . show) (cliMaxLength opts) maxLen' = cliLength opts >> cliMaxLength opts num = maybe [] (("-n":) . singleton . show) (cliNumber opts) phrase = ["-p" | cliPassphrase opts]+ specials = ["-s" | cliSpecials opts] -- |Run elocrypt with the given options run :: CliOptions -> IO (Handle, Handle, Handle, ProcessHandle)@@ -142,7 +154,7 @@ fromPositive = fmap getPositive readHandle :: Handle -> IO String-readHandle = (<$>) asUtf8Str . waitOutput (seconds 2) 5000+readHandle = (<$>) asUtf8Str . waitOutput (seconds 5) 5000 assertExitedSuccess :: Timeout -> ProcessHandle -> IO Bool assertExitedSuccess t = fmap (== ExitSuccess) . assertExitedTimeout t
+ test/Test/Elocrypt/UtilsTest.hs view
@@ -0,0 +1,90 @@+{-# LANGUAGE TemplateHaskell #-}+module Test.Elocrypt.UtilsTest where++import Control.Monad.Random+import Data.Maybe (isNothing)+import Data.List (find, elem, sort)+import Data.Ratio ((%))+import qualified Data.Map as M++import Test.QuickCheck+import Test.QuickCheck.Monadic (monadicIO)+import Test.Tasty+import Test.Tasty.QuickCheck (testProperty)+import Test.Tasty.TH++import Data.Elocrypt.Utils+import Test.Elocrypt.QuickCheck+import Test.Elocrypt.Instances++tests :: TestTree+tests = $(testGroupGenerator)++-- |Maps letters to all digits 0-9+prop_numeralConversionsHasAllDigits :: Bool+prop_numeralConversionsHasAllDigits+ = sort digits == ['0'..'9']+ where digits = M.foldr (++) [] numeralConversions++-- |toDigit never returns a letter that looks like a number+prop_toDigitDoesntLookLikeNumber :: Char -> Bool+prop_toDigitDoesntLookLikeNumber c+ = isNothing $ find (`elem` candidates) c'+ where candidates = M.keys numeralConversions+ c' = toDigit c++-- |toSymbol never returns a letter that looks like a symbol+prop_toSymbolDoesntLookLikeSymbol :: Char -> Bool+prop_toSymbolDoesntLookLikeSymbol c+ = isNothing $ find (`elem` M.keys symbolConversions) c'+ where c' = toSymbol c++prop_toSymbolReturnsSymbols :: Bool+prop_toSymbolReturnsSymbols = all isSymbol ss+ where ks = M.keys symbolConversions+ ss = concatMap toSymbol ks++-- |isSymbol returns false for letters and numbers+prop_isSymbolAlphaNumReturnsFalse :: Char -> Property+prop_isSymbolAlphaNumReturnsFalse c+ = c `elem` (['a'..'z'] ++ ['A'..'Z'] ++ ['0'..'9']) ==> not (isSymbol c)++prop_isSymbolWhitespaceReturnsFalse :: Bool+prop_isSymbolWhitespaceReturnsFalse = all (not . isSymbol) " \n"++-- |updateR always updates when prob = 1/0+prop_updateRAlwaysUpdates :: String -> StdGen -> Bool+prop_updateRAlwaysUpdates s = evalRand $ do+ let f _ = return '*'+ prob = 99999 % 1 -- Make prob absurdly high since we can't use 0 denoms+ s' <- updateR f prob s++ -- Make sure almost all letters are '*'+ return $ length (filter (=='*') s') >= length s - 1++-- |updateR never updates when prob = 0/1+prop_updateRNeverUpdates :: String -> StdGen -> Bool+prop_updateRNeverUpdates s = evalRand $ do+ let f _ = return '*'+ prob = 0 % 1+ s' <- updateR f prob s++ return $ s == s'++-- |update1 Always updates at least one character+prop_update1AlwaysUpdates1 :: String -> Property+prop_update1AlwaysUpdates1 s = not (null s) ==> do+ let f = (const . return) '*'+ + pos <- choose (0, length s - 1)+ s' <- update1 f s pos++ return $ elem '*' s'++-- |update1 allows empty string+prop_update1DoesntCrashOnEmpty :: Property+prop_update1DoesntCrashOnEmpty = monadicIO $ do+ let f _ = return '*'+ s <- update1 f "" 0++ return (s == "")
test/Test/ElocryptTest.hs view
@@ -4,7 +4,7 @@ import Control.Monad import Control.Monad.Random import Data.Bool-import Data.Char+import Data.Char hiding (isSymbol) import Data.List import Data.Maybe import Test.QuickCheck@@ -14,6 +14,7 @@ import Data.Elocrypt import Data.Elocrypt.Trigraph+import Data.Elocrypt.Utils (isSymbol) import Test.Elocrypt.QuickCheck import Test.Elocrypt.Instances @@ -42,11 +43,36 @@ -- |Most passwords have more than one capital prop_newPasswordHasMultipleCaps :: Positive Int -> StdGen -> Property prop_newPasswordHasMultipleCaps (Positive len) gen =- cover 50 (length (filter isUpper pass) > 1) "has multiple caps"- $ any isLower pass+ checkCoverage $+ cover 50 (length (filter isUpper pass) > 1) "has multiple caps" True where pass = newPassword (len + 2) opts gen opts = genOptions { genCapitals = True }++-- |Passwords are all letters when numbers is false+prop_newPasswordIsAlpha :: Positive Int -> StdGen -> Bool+prop_newPasswordIsAlpha (Positive len) gen = all isAlpha pass+ where+ pass = newPassword len opts gen+ opts = genOptions { genDigits = False }++-- |Most passwords have at least one number+prop_newPasswordHasDigits :: Positive Int -> StdGen -> Property+prop_newPasswordHasDigits (Positive len) gen =+ checkCoverage $+ cover 50 (any isDigit pass) "has digits" True+ where+ pass = newPassword (len + 2) opts gen+ opts = genOptions { genDigits = True }++-- |Most passwords have at least one special character+prop_newPasswordHasSpecials :: Positive Int -> StdGen -> Property+prop_newPasswordHasSpecials (Positive len) gen =+ checkCoverage $+ cover 50 (any isSymbol pass) "has specials" True+ where+ pass = newPassword (len + 2) opts gen+ opts = genOptions { genSpecials = True } -- |Third and each successive character is taken from the trigraph prop_3rdCharHasPositiveFrequency
test/Tests.hs view
@@ -5,10 +5,12 @@ import qualified Test.ElocryptTest as PasswordTest import qualified Test.Elocrypt.TrigraphTest as TrigraphTest+import qualified Test.Elocrypt.UtilsTest as UtilsTest main :: IO () main = defaultMain tests tests :: TestTree tests = testGroup "Unit Tests" [PasswordTest.tests,- TrigraphTest.tests]+ TrigraphTest.tests,+ UtilsTest.tests]