packages feed

apache-md5 0.5.0.1 → 0.6.0.0

raw patch · 8 files changed

+358/−161 lines, 8 filesdep +deepseqdep ~basePVP ok

version bump matches the API change (PVP)

Dependencies added: deepseq

Dependency ranges changed: base

API changes (from Hackage documentation)

- Data.Digest.ApacheMD5: alpha64 :: ByteString
- Data.Digest.ApacheMD5: apacheMD5' :: (ByteString -> ByteString) -> ByteString -> ByteString -> ByteString
- Data.Digest.ApacheMD5: encode64 :: ByteString -> ByteString
- Data.Digest.ApacheMD5: md5DigestLength :: Int
+ Data.Digest.ApacheMD5: data Salt
+ Data.Digest.ApacheMD5: mkSalt :: ByteString -> Maybe Salt
+ Data.Digest.ApacheMD5: type Password = ByteString
+ Data.Digest.ApacheMD5: unSalt :: Salt -> ByteString
+ Data.Digest.ApacheMD5.Internal: Salt :: ByteString -> Salt
+ Data.Digest.ApacheMD5.Internal: alpha64 :: ByteString
+ Data.Digest.ApacheMD5.Internal: apacheMD5 :: (ByteString -> ByteString) -> Password -> Salt -> ByteString
+ Data.Digest.ApacheMD5.Internal: encode64 :: ByteString -> ByteString
+ Data.Digest.ApacheMD5.Internal: instance Constructor C1_0Salt
+ Data.Digest.ApacheMD5.Internal: instance Data Salt
+ Data.Digest.ApacheMD5.Internal: instance Datatype D1Salt
+ Data.Digest.ApacheMD5.Internal: instance Eq Salt
+ Data.Digest.ApacheMD5.Internal: instance Generic Salt
+ Data.Digest.ApacheMD5.Internal: instance Ord Salt
+ Data.Digest.ApacheMD5.Internal: instance Read Salt
+ Data.Digest.ApacheMD5.Internal: instance Show Salt
+ Data.Digest.ApacheMD5.Internal: instance Typeable Salt
+ Data.Digest.ApacheMD5.Internal: isAlpha64 :: Word8 -> Bool
+ Data.Digest.ApacheMD5.Internal: md5BS :: ByteString -> ByteString
+ Data.Digest.ApacheMD5.Internal: md5DigestLength :: Int
+ Data.Digest.ApacheMD5.Internal: newtype Salt
+ Data.Digest.ApacheMD5.Internal: type Password = ByteString
- Data.Digest.ApacheMD5: apacheMD5 :: ByteString -> ByteString -> ByteString
+ Data.Digest.ApacheMD5: apacheMD5 :: Password -> Salt -> ByteString

Files

ChangeLog.md view
@@ -1,5 +1,18 @@ # ChangeLog / ReleaseNotes +## Version 0.6.0.0++* Introducing Salt newtype wrapper to guarantee that it consists of only+  characters that can be used in htpasswd entry.+* Algorithm implementation details and helper functions were all moved to+  `Data.Digest.ApacheMD5.Internal` module to make it explicit that they aren't+  part of stable API.+* Exposing `Data.Digest.ApacheMD5.Internal` module so that library authors+  still have the ability to get the most out of this package.+* Updated `README.md`, `example.hs`, tests and benchmarks.+* Uploaded to [Hackage][]:+  <http://hackage.haskell.org/package/apache-md5-0.6.0.0>+  ## Version 0.5.0.1 
README.md view
@@ -51,21 +51,28 @@ import qualified Data.ByteString as BS (index, pack) import qualified Data.ByteString.Char8 as C8 (concat, pack, putStrLn, singleton)     -- bytestring package http://hackage.haskell.org/package/bytestring-import Data.Digest.ApacheMD5 (alpha64, apacheMD5)+import Data.Digest.ApacheMD5 (Salt, alpha64, apacheMD5, mkSalt, unSalt)  -htpasswdEntry :: String -> String -> ByteString -> ByteString+htpasswdEntry :: String -> String -> Salt -> ByteString htpasswdEntry username password salt = C8.concat     [ C8.pack username     , C8.pack ":$apr1$"-    , salt+    , unSalt salt     , C8.singleton '$'     , apacheMD5 (C8.pack password) salt     ] -genSalt :: IO ByteString-genSalt = evalRandIO-    $ BS.pack . map (BS.index alpha64) . take 8 <$> getRandomRs (0, 63)+genSalt :: IO Salt+genSalt = do+    Just s <- evalRandIO $ mkSalt . BS.pack . map (BS.index alpha64) . take 8+        <$> getRandomRs (0, 63)+        -- We know that Salt is correctly generated, since we use alpha64 to do+        -- it. That is the reason why we can pattern match on Just.+        --+        -- Other option would be to use Salt value constructor from+        -- Data.Digest.ApacheMD5.Internal module.+    return s  main :: IO () main = do@@ -107,6 +114,22 @@     $ cabal configure --enable-tests && cabal build && cabal test  +Benchmarks+----------++This package provides [Criterion][] benchmarks, to run them you can use+something like:++    $ cabal configure --enable-benchmarks && cabal build && cabal bench++To generate HTML output one needs to specify output file. Then the last+command in above chain would look like:++    $ cabal bench --benchmark-option=--output=benchmarks.html++Where `benchmarks.html` is the name of [Criterion][] generated HTML file.++ Contributions ------------- @@ -121,6 +144,8 @@     http://packages.debian.org/lenny/libssl-dev [cabal-install]:     http://haskell.org/haskellwiki/Cabal-Install+[Criterion]:+    http://hackage.haskell.org/package/criterion [Hackage]:     http://hackage.haskell.org/package/apache-md5 [HaskellWiki: How to install a Cabal package]:
apache-md5.cabal view
@@ -1,5 +1,5 @@ name:                 apache-md5-version:              0.5.0.1+version:              0.6.0.0 synopsis:             Apache specific MD5 digest algorighm. description:   Haskell implementation of Apache HTTP server specific MD5 digest algorithm@@ -11,9 +11,11 @@   .   * <https://github.com/trskop/apache-md5/blob/master/ChangeLog.md> +homepage:             https://github.com/trskop/apache-md5+bug-reports:          https://github.com/trskop/apache-md5/issues license:              BSD3 license-File:         LICENSE-copyright:            (c) 2009, 2010, 2012, 2013 Peter Trško+copyright:            (c) 2009, 2010, 2012 - 2014 Peter Trško author:               Peter Trško <peter.trsko@gmail.com> maintainer:           peter.trsko@gmail.com category:             Data, Cryptography@@ -31,15 +33,28 @@     Pass additional warning flags including -Werror to GHC during compilation.   default:            False +flag deepseq+  description:+    Define instance of NFData for Salt newtype. This dependency is enforced for+    benchmark.+  default:            False+ library   hs-Source-Dirs:       src-  exposed-Modules:      Data.Digest.ApacheMD5-  other-Modules:        Data.Digest.ApacheMD5.Internal-  build-Depends:+  exposed-modules:+      Data.Digest.ApacheMD5+    , Data.Digest.ApacheMD5.Internal+  build-depends:       base >= 4 && < 5     , bytestring >= 0.10 && < 0.11++  if flag(deepseq)+    cpp-options:        -DWITH_deepseq+    build-depends:      deepseq >= 1.1.0.0+    -- Same minimal bound as criterion has since version 0.4.0.+   includes:             openssl/md5.h-  extra-Libraries:      crypto+  extra-libraries:      crypto    ghc-options:          -Wall   if impl(ghc >= 6.8)@@ -104,11 +119,14 @@     , bytestring >= 0.10 && < 0.11      , criterion+    , deepseq >= 1.1.0.0+    -- Same minimal bound as criterion has since version 0.4.0.     , MonadRandom    includes:             openssl/md5.h   extra-libraries:      crypto +  cpp-options:          -DWITH_deepseq   ghc-options:          -Wall   if impl(ghc >= 6.8)     ghc-options:        -fwarn-tabs@@ -122,4 +140,4 @@ source-repository this   type:                 git   location:             git://github.com/trskop/apache-md5.git-  tag:                  v0.5.0.1+  tag:                  v0.6.0.0
example.hs view
@@ -1,7 +1,7 @@ -- | -- Module:       Main -- Description:  Create htpasswd like entry and print it to stdout.--- Copyright:    (c) 2013 Peter Trsko+-- Copyright:    (c) 2013, 2014 Peter Trsko -- License:      BSD3 -- -- Maintainer:   peter.trsko@gmail.com@@ -23,21 +23,28 @@ import qualified Data.ByteString as BS (index, pack) import qualified Data.ByteString.Char8 as C8 (concat, pack, putStrLn, singleton)     -- bytestring package http://hackage.haskell.org/package/bytestring-import Data.Digest.ApacheMD5 (alpha64, apacheMD5)+import Data.Digest.ApacheMD5 (Salt, alpha64, apacheMD5, mkSalt, unSalt)  -htpasswdEntry :: String -> String -> ByteString -> ByteString+htpasswdEntry :: String -> String -> Salt -> ByteString htpasswdEntry username password salt = C8.concat     [ C8.pack username     , C8.pack ":$apr1$"-    , salt+    , unSalt salt     , C8.singleton '$'     , apacheMD5 (C8.pack password) salt     ] -genSalt :: IO ByteString-genSalt = evalRandIO-    $ BS.pack . map (BS.index alpha64) . take 8 <$> getRandomRs (0, 63)+genSalt :: IO Salt+genSalt = do+    Just s <- evalRandIO $ mkSalt . BS.pack . map (BS.index alpha64) . take 8+        <$> getRandomRs (0, 63)+        -- We know that Salt is correctly generated, since we use alpha64 to do+        -- it. That is the reason why we can pattern match on Just.+        --+        -- Other option would be to use Salt value constructor from+        -- Data.Digest.ApacheMD5.Internal module.+    return s  main :: IO () main = do
src/Data/Digest/ApacheMD5.hs view
@@ -1,11 +1,10 @@-{-# LANGUAGE BangPatterns #-} -- | -- Module:      Data.Digest.ApacheMD5--- Copyright:   (c) 2009, 2010, 2012, 2013 Peter Trško+-- Copyright:   (c) 2009, 2010, 2012 - 2014 Peter Trško -- License:     BSD3 -- Maintainer:  Peter Trško <peter.trsko@gmail.com> -- Stability:   Provisional--- Portability: non-portable (BangPatterns)+-- Portability: non-portable (depends on non-portable internal module) -- -- ApacheMD5 is one of the hash algorithms used by Apache HTTP server for basic -- authentication. It is Apache specific, but e.g. nginx supports this@@ -35,128 +34,55 @@     --     -- > import Data.ByteString (ByteString)     -- > import qualified Data.ByteString.Char8 as C8 (concat, pack, singleton)-    -- > import Data.Digest.ApacheMD5 (apacheMD5)+    -- > import Data.Digest.ApacheMD5 (Salt, apacheMD5, unSalt)     -- >-    -- > htpasswdEntry :: ByteString -> ByteString -> ByteString -> ByteString+    -- > htpasswdEntry :: ByteString -> ByteString -> Salt -> ByteString     -- > htpasswdEntry username password salt = C8.concat     -- >     [ username     -- >     , C8.pack ":$apr1$"-    -- >     , salt+    -- >     , unSalt salt     -- >     , C8.singleton '$'     -- >     , apacheMD5 password salt     -- >     ]      -- * API Documentation       apacheMD5-    , apacheMD5'-    , alpha64-    , encode64-    , md5DigestLength+    , Password+    , Salt+    , mkSalt+    , unSalt     )   where -import Data.Bits (Bits((.|.), (.&.), shiftL, shiftR))-import Data.Word (Word8, Word16, Word32)- import Data.ByteString (ByteString)-import qualified Data.ByteString as BS-    ( append-    , concat-    , cons-    , empty-    , head-    , index-    , length-    , null-    , pack-    , take+import qualified Data.ByteString as BS (all)++import Data.Digest.ApacheMD5.Internal (Password, Salt(Salt))+import qualified Data.Digest.ApacheMD5.Internal as Internal+    ( apacheMD5+    , encode64+    , isAlpha64+    , md5BS     )-import qualified Data.ByteString.Char8 as C8 (pack) -import Data.Digest.ApacheMD5.Internal (md5BS, md5DigestLength) +-- | Smart constructor for 'Salt'. It tests all octets to be members of+-- 'Data.Digest.ApacheMD5.Internal.alpha64' by using 'Internal.isAlpha64'+-- predicate.+mkSalt :: ByteString -> Maybe Salt+mkSalt str+  | BS.all Internal.isAlpha64 str = Just $ Salt str+  | otherwise                     = Nothing +-- | Unpack 'Salt' in to 'ByteString'.+unSalt :: Salt -> ByteString+unSalt (Salt str) = str+ -- | Taking password and salt this function produces resulting ApacheMD5 hash -- which is already base 64 encoded.-apacheMD5 :: ByteString -> ByteString -> ByteString-apacheMD5 = (encode64 .) . apacheMD5' md5BS---- | Raw Apache MD5 implementation that is parametrized by MD5 implementation--- and doesn't encode result in to base 64.-apacheMD5'-    :: (ByteString -> ByteString)-    -- ^ MD5 hash function.-    -> ByteString-    -- ^ Password-    -> ByteString-    -- ^ Salt+apacheMD5+    :: Password+    -> Salt     -> ByteString     -- ^ Apache MD5 Hash-apacheMD5' md5 !password !salt = g . f . md5 $ password <> salt <> password-  where-    (<>) = BS.append--    f :: ByteString -> ByteString-    f !digest = md5 $ password <> C8.pack "$apr1$" <> salt-        <> BS.concat (replicate (passwordLength `div` md5DigestLength) digest)-        <> BS.take (passwordLength `rem` md5DigestLength) digest-        <> f' pwHead passwordLength-      where-        !passwordLength = BS.length password-        pwHead = if BS.null password then 0 else BS.head password-            -- Consistent with htpasswd implementation.--        f' :: Word8 -> Int -> ByteString-        f' !pwhead !i-            | i == 0    = BS.empty-            | otherwise = (if i .&. 1 == 1 then 0 else pwhead)-                `BS.cons` f' pwhead (i `shiftR` 1)--    g :: ByteString -> ByteString-    g = g' 0-      where-        -- Iterate this function 1000 times, starting with 0 and ending with-        -- 999.-        g' :: Word16 -> ByteString -> ByteString-        g' !i !digest-          | i < 1000 = g' (i + 1) . md5-            $  (if i .&. 1 == 1  then password else digest)-            <> (if i `mod` 3 > 0 then salt     else BS.empty)-            <> (if i `mod` 7 > 0 then password else BS.empty)-            <> (if i .&. 1 == 1  then digest   else password)-          | otherwise = digest---- | Alphabet used by 'encode64'.-alpha64 :: ByteString-alpha64 = C8.pack-    "./0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz"--encode64 :: ByteString -> ByteString-encode64 str = BS.pack $ concatMap (encode64' str)-  -- Index --.   ,-- Shift bits left this much-  --         V   V-    [ (4, [( 0, 16), ( 6, 8), (12, 0)])-    , (4, [( 1, 16), ( 7, 8), (13, 0)])-    , (4, [( 2, 16), ( 8, 8), (14, 0)])-    , (4, [( 3, 16), ( 9, 8), (15, 0)])-    , (4, [( 4, 16), (10, 8), ( 5, 0)])-    , (2, [(11,  0)                  ])-  --   ^  `-----------. ,------------'-  --   |               V-  --   |    Do bitwise OR on results-  --   |-  --   `-- How many characters from alpa64-  --       will be used to encode the row.-    ]-  where-    encode64' :: ByteString -> (Int, [(Int, Int)]) -> [Word8]-    encode64' !s (!n, xs) =-        to64 n . foldl1 (.|.) . (`map` xs) $ \ (!i, !t) ->-            conv (s `BS.index` i) `shiftL` t--    conv :: (Integral i, Num n, Integral n, Bits n) => i -> n-    conv = fromInteger . toInteger--    to64 :: Int -> Word32 -> [Word8]-    to64 !n !c = take n . map ((alpha64 `BS.index`) . conv . (.&. 0x3f))-        $ iterate (`shiftR` 6) c+apacheMD5 = (Internal.encode64 .) . Internal.apacheMD5 Internal.md5BS
src/Data/Digest/ApacheMD5/Internal.hs view
@@ -1,21 +1,208 @@+{-# LANGUAGE CPP #-}+{-# LANGUAGE BangPatterns #-}++#ifdef __GLASGOW_HASKELL__+#define LANGUAGE_DERIVE_DATA_TYPEABLE+{-# LANGUAGE DeriveDataTypeable #-}+#endif++#if defined(__GLASGOW_HASKELL__) && __GLASGOW_HASKELL__ >= 704+#define LANGUAGE_DERIVE_GENERIC+{-# LANGUAGE DeriveGeneric #-}+#endif+ {-# LANGUAGE ForeignFunctionInterface #-}++-- |+-- Module:      Data.Digest.ApacheMD5.Internal+-- Copyright:   (c) 2009, 2010, 2012 - 2014 Peter Trško+-- License:     BSD3+-- Maintainer:  Peter Trško <peter.trsko@gmail.com>+-- Stability:   Provisional+-- Portability: non-portable (BangPatterns, CPP, DeriveDataTypeable,+--              DeriveGeneric, ForeignFunctionInterface)+--+-- Internal and unsafe functions used for implementing Apache MD5+-- hash algorithm.+--+-- Try to avoid using this module directly when possible, but there+-- are situations when it might come handy. module Data.Digest.ApacheMD5.Internal-    ( md5BS+    (+    -- * Types+      Password+    , Salt(Salt)++    -- * ApacheMD5 Hash+    , apacheMD5++    -- * Base64-like encoding+    , alpha64+    , isAlpha64+    , encode64++    -- * OpenSSL Bindings+    , md5BS     , md5DigestLength     )   where +import Control.Applicative (liftA2) import Control.Monad (void)-import Data.Word (Word8)+import Data.Bits (Bits((.|.), (.&.), shiftL, shiftR))+import Data.Word (Word8, Word16, Word32) import Foreign (Ptr) import Foreign.C.Types (CChar(..), CULong(..)) import System.IO.Unsafe (unsafePerformIO) +#ifdef WITH_deepseq+import Control.DeepSeq (NFData)+#endif++#ifdef LANGUAGE_DERIVE_DATA_TYPEABLE+import Data.Data (Data)+import Data.Typeable (Typeable)+#endif++#ifdef LANGUAGE_DERIVE_GENERIC+import GHC.Generics (Generic)+#endif+ import Data.ByteString (ByteString)+import qualified Data.ByteString as BS+    ( append+    , concat+    , cons+    , empty+    , head+    , index+    , length+    , null+    , pack+    , take+    )+import qualified Data.ByteString.Char8 as C8 (pack) import qualified Data.ByteString.Internal as BS (create) import qualified Data.ByteString.Unsafe as BS (unsafeUseAsCStringLen)  +-- | Type alias for more readable type signatures.+type Password = ByteString++-- | Apache MD5 hash salt. When constructing @.htpasswd@ file it is necessary+-- for the salt to be consisting of octets from 'alpha64' \"set\". This newtype+-- along with 'mkSalt' smart constructor are here to ensure such invariant.+newtype Salt = Salt ByteString+  deriving+    ( Eq, Ord, Read, Show+#ifdef LANGUAGE_DERIVE_DATA_TYPEABLE+    , Data, Typeable+#endif+#ifdef LANGUAGE_DERIVE_GENERIC+    , Generic+#endif+    )++#ifdef WITH_deepseq+instance NFData Salt+#endif++-- | Raw Apache MD5 implementation that is parametrized by MD5 implementation+-- and doesn't encode result in to base 64.+apacheMD5+    :: (ByteString -> ByteString)+    -- ^ MD5 hash function.+    -> Password+    -> Salt+    -> ByteString+    -- ^ Apache MD5 Hash+apacheMD5 md5 !password (Salt !salt) =+    g . f . md5 $ password <> salt <> password+  where+    (<>) = BS.append++    f :: ByteString -> ByteString+    f !digest = md5 $ password <> C8.pack "$apr1$" <> salt+        <> BS.concat (replicate (passwordLength `div` md5DigestLength) digest)+        <> BS.take (passwordLength `rem` md5DigestLength) digest+        <> f' pwHead passwordLength+      where+        !passwordLength = BS.length password+        pwHead = if BS.null password then 0 else BS.head password+            -- Consistent with htpasswd implementation.++        f' :: Word8 -> Int -> ByteString+        f' !pwhead !i+          | i == 0    = BS.empty+          | otherwise = (if i .&. 1 == 1 then 0 else pwhead)+            `BS.cons` f' pwhead (i `shiftR` 1)++    g :: ByteString -> ByteString+    g = g' 0+      where+        -- Iterate this function 1000 times, starting with 0 and ending with+        -- 999.+        g' :: Word16 -> ByteString -> ByteString+        g' !i !digest+          | i < 1000 = g' (i + 1) . md5+            $  (if i .&. 1 == 1  then password else digest)+            <> (if i `mod` 3 > 0 then salt     else BS.empty)+            <> (if i `mod` 7 > 0 then password else BS.empty)+            <> (if i .&. 1 == 1  then digest   else password)+          | otherwise = digest++-- | Alphabet used by 'encode64'.+alpha64 :: ByteString+alpha64 = C8.pack+    "./0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz"++-- | Check if specified 8 bit word is a valid member of 'alpha64'.+isAlpha64 :: Word8 -> Bool+isAlpha64 = ((>= dot) <&&> (<= _9))+    <||> ((>= _A) <&&> (<= _Z))+    <||> ((>= _a) <&&> (<= _z))+  where+    (<&&>) = liftA2 (&&)+    (<||>) = liftA2 (||)++    dot = 46 -- '.'+    _9 = 57  -- '9'+    _A = 65  -- 'A'+    _Z = 90  -- 'Z'+    _a = 97  -- 'a'+    _z = 122 -- 'z'++-- | Encode raw MD5 hash in to Base64-like encoding.+encode64 :: ByteString -> ByteString+encode64 str = BS.pack $ concatMap (encode64' str)+  -- Index --.   ,-- Shift bits left this much+  --         V   V+    [ (4, [( 0, 16), ( 6, 8), (12, 0)])+    , (4, [( 1, 16), ( 7, 8), (13, 0)])+    , (4, [( 2, 16), ( 8, 8), (14, 0)])+    , (4, [( 3, 16), ( 9, 8), (15, 0)])+    , (4, [( 4, 16), (10, 8), ( 5, 0)])+    , (2, [(11,  0)                  ])+  --   ^  `-----------. ,------------'+  --   |               V+  --   |    Do bitwise OR on results+  --   |+  --   `-- How many characters from alpa64+  --       will be used to encode the row.+    ]+  where+    encode64' :: ByteString -> (Int, [(Int, Int)]) -> [Word8]+    encode64' !s (!n, xs) =+        to64 n . foldl1 (.|.) . (`map` xs) $ \ (!i, !t) ->+            conv (s `BS.index` i) `shiftL` t++    conv :: (Integral i, Num n, Integral n, Bits n) => i -> n+    conv = fromInteger . toInteger++    to64 :: Int -> Word32 -> [Word8]+    to64 !n !c = take n . map ((alpha64 `BS.index`) . conv . (.&. 0x3f))+        $ iterate (`shiftR` 6) c+ -- Inspired by nano-md5 <http://hackage.haskell.org/package/nano-md5> package -- by Don Stewart --@@ -26,9 +213,11 @@ foreign import ccall "openssl/md5.h MD5"     c_md5 :: Ptr CChar -> CULong -> Ptr Word8 -> IO (Ptr Word8) +-- | Length of MD5 hash in octets. md5DigestLength :: Int md5DigestLength = 16 +-- | Thin Haskell wrapper around OpenSSL's MD5 hash function. md5BS :: ByteString -> ByteString md5BS bs = unsafePerformIO . BS.unsafeUseAsCStringLen bs $ \ (ptr, len) ->     BS.create md5DigestLength $ void . c_md5 ptr (fromIntegral len)
test/benchmark-main.hs view
@@ -3,29 +3,34 @@     where  import Control.Applicative  ((<$>))-import Control.Arrow ((***), first) import Control.Monad (replicateM)-import Data.Word (Word8) +import Data.ByteString (ByteString)+ import Control.Monad.Random-import Criterion.Main+import Criterion.Config (Config(cfgPerformGC), defaultConfig, ljust)+import Criterion.Main (bench, defaultMainWith, nf) import qualified Data.ByteString as BS import qualified Data.ByteString.Char8 as C8 -import Data.Digest.ApacheMD5 (alpha64, apacheMD5)+import Data.Digest.ApacheMD5 (apacheMD5)+import Data.Digest.ApacheMD5.Internal (Salt(Salt), alpha64)  -genSalt :: RandT StdGen IO [Word8]-genSalt = replicateM 8 $ BS.index alpha64 <$> getRandomR (0, 63)+genSalt :: RandT StdGen IO Salt+genSalt = Salt . BS.pack+    <$> replicateM 8 (BS.index alpha64 <$> getRandomR (0, 63))+    -- We know, that salt will be correct since we generate it out of+    -- alpha64, therefore we don't use mkSalt to check it for us.  genPassword :: Int -> RandT StdGen IO String genPassword len = replicateM len (getRandomR ('!', '~')) -genData :: Int -> RandT StdGen IO ([Word8], String)+genData :: Int -> RandT StdGen IO (ByteString, Salt) genData len = do     s <- genSalt-    p <- genPassword len-    return (s, p)+    p <- C8.pack <$> genPassword len+    return (p, s)  main :: IO () main = do@@ -35,17 +40,17 @@     (!inputData64, _) <- genData' 64     (!inputData128, _) <- genData' 128     (!inputData256, _) <- genData' 256+    (!inputData512, _) <- genData' 512 -    defaultMain+    defaultMainWith defaultConfig{cfgPerformGC = ljust True} (return ())         [ bench "Random passwords of length 8" $ test inputData8         , bench "Random passwords of length 16" $ test inputData16         , bench "Random passwords of length 32" $ test inputData32         , bench "Random passwords of length 64" $ test inputData64         , bench "Random passwords of length 128" $ test inputData128         , bench "Random passwords of length 256" $ test inputData256+        , bench "Random passwords of length 512" $ test inputData512         ]   where     test = nf $ uncurry apacheMD5--    genData' n = getStdGen-        >>= (first (BS.pack *** C8.pack) <$>) . runRandT (genData n)+    genData' n = getStdGen >>= runRandT (genData n)
test/unit-tests-main.hs view
@@ -1,21 +1,23 @@ module Main (main)     where -import Control.Arrow ((***), second)+import Control.Arrow (Arrow(second)) import Control.Monad (replicateM, replicateM_, void, when) import Control.Monad.IO.Class (liftIO) import Control.Monad.Random import qualified Data.ByteString.Char8 as C8+import Data.Function (on) import System.Exit (ExitCode(..)) import System.Process (readProcessWithExitCode) import Test.Framework-import Test.Framework.Providers.HUnit-import Test.HUnit+import Test.Framework.Providers.HUnit (testCase)+import Test.HUnit (Assertion, assertEqual)  import Data.Digest.ApacheMD5 (apacheMD5)+import Data.Digest.ApacheMD5.Internal (Salt(Salt))  --- Settings -------------------------------------------------------------------+-- {{{ Settings ---------------------------------------------------------------  numberOfTests :: Int numberOfTests = 1000@@ -23,6 +25,10 @@ maxPasswordLength :: Int maxPasswordLength = 255 +-- }}} Settings ---------------------------------------------------------------++-- | Execute @htpasswd@ command to generate salt and Apache MD5 hash for given+-- password. runHtpasswd     :: String     -- ^ Password@@ -41,7 +47,7 @@             ]     return $ parse out     where-        -- Split "<username>:$apr1$<salt>$<hash>\n" to pair (Salt,Hash)+        -- Split "<username>:$apr1$<salt>$<hash>\n" to pair (Salt, Hash)         parse :: String -> (String, String)         parse = second (drop 1)      -- ("<salt>","<hash>")             . break (== '$')         -- ("<salt>","$<hash>")@@ -49,14 +55,21 @@             . dropWhile (/= ':')     -- ":$apr1$<salt>$<hash>"             . takeWhile (/= '\n')    -- "<username>:$apr1$<salt>$<hash>" -testApacheMD5' :: (String -> String -> String) -> Int -> RandT StdGen IO ()-testApacheMD5' toTest n = replicateM_ n $ do-    password <- genPassword-    (salt, hash) <- liftIO $ runHtpasswd password--    liftIO $ assertEqual (msg salt password) hash-        $ toTest password salt+-- | Generate random password, run @htpasswd@ to obtain salt and reference+-- Apache MD5, then ran provided function with password and salt as arguments+-- and compare its output to what @htpasswd@ generated.+testApacheMD5 :: (String -> String -> String) -> Int -> Assertion+testApacheMD5 toTest n =+    void $ getStdGen >>= runRandT (replicateM_ n testApacheMD5')   where+    testApacheMD5' :: RandT StdGen IO ()+    testApacheMD5' = do+        password <- genPassword+        (salt, hash) <- liftIO $ runHtpasswd password++        liftIO $ assertEqual (msg salt password) hash+            $ toTest password salt+     msg :: String -> String -> String     msg s p = concat         [ "Hash does not match for salt = "@@ -65,17 +78,18 @@         , show p         ] +    -- Generate password of length between 0 and maxPasswordLength.     genPassword :: RandT StdGen IO String     genPassword = getRandomR (0, maxPasswordLength)-        >>= flip replicateM (getRandomR ('!', '~'))--testApacheMD5 :: (String -> String -> String) -> Int -> Assertion-testApacheMD5 f n = void $ getStdGen >>= runRandT (testApacheMD5' f n)+        >>= (`replicateM` getRandomR ('!', '~'))  main :: IO ()-main = defaultMain . hUnitTestToTests $ TestList-    [ TestLabel ("apacheMD5 (" ++ show numberOfTests ++ " tests)")-        . TestCase $ testApacheMD5 apacheMD5' numberOfTests+main = defaultMain+    [ testCase ("apacheMD5 (" ++ show numberOfTests ++ " tests)")+        $ testApacheMD5 apacheMD5' numberOfTests     ]   where-    apacheMD5' = curry $ C8.unpack . uncurry apacheMD5 . (C8.pack *** C8.pack)+    -- Wrap tested function so that it takes Strings and produces String.+    apacheMD5' :: String -> String -> String+    apacheMD5' =+        (C8.unpack .) . ((\password -> apacheMD5 password . Salt) `on` C8.pack)