packages feed

password 3.0.1.0 → 3.0.2.0

raw patch · 12 files changed

+168/−64 lines, 12 filesPVP ok

version bump matches the API change (PVP)

API changes (from Hackage documentation)

+ Data.Password.Argon2: extractParams :: PasswordHash Argon2 -> Maybe Argon2Params
+ Data.Password.Bcrypt: extractParams :: PasswordHash Bcrypt -> Maybe Int
+ Data.Password.PBKDF2: extractParams :: PasswordHash PBKDF2 -> Maybe PBKDF2Params
+ Data.Password.Scrypt: extractParams :: PasswordHash Scrypt -> Maybe ScryptParams

Files

ChangeLog.md view
@@ -1,5 +1,11 @@ # Changelog for `password` +## 3.0.2.0++-   Add `extractParams` on `PasswordHash`s+    Thanks to [@blackheaven](https://github.com/blackheaven)+    [#61](https://github.com/cdepillabout/password/pull/61)+ ## 3.0.1.0  -   Argon2 hashes without a version field are interpreted as being of version 1.0
password.cabal view
@@ -1,7 +1,7 @@ cabal-version: 1.12  name:           password-version:        3.0.1.0+version:        3.0.2.0 category:       Data synopsis:       Hashing and checking of passwords description:@@ -71,9 +71,9 @@   build-depends:       base        >= 4.9      && < 5     , base64      >= 0.3      && < 0.5-    , bytestring  >= 0.10.8.1 && < 0.11-    , cryptonite  >= 0.15.1   && < 0.30-    , memory      >= 0.14     && < 0.17+    , bytestring  >= 0.10.8.1 && < 0.12+    , cryptonite  >= 0.15.1+    , memory      >= 0.14     , password-types             < 2     , template-haskell     , text        >= 1.2.2    && < 1.3
src/Data/Password/Argon2.hs view
@@ -52,6 +52,7 @@   -- * Hashing Manually (Argon2)   , hashPasswordWithParams   , defaultParams+  , extractParams   , Argon2Params(..)   , Argon2.Variant(..)   , Argon2.Version(..)@@ -81,7 +82,7 @@ import Data.Semigroup ((<>)) #endif import Data.Text (Text)-import qualified Data.Text as T (intercalate, split, splitAt, stripPrefix)+import qualified Data.Text as T (dropWhileEnd, intercalate, split, splitAt, stripPrefix) import Data.Word (Word32)  import Data.Password.Internal (@@ -91,7 +92,6 @@     showT,     toBytes,     unsafePad64,-    unsafeRemovePad64,  ) import Data.Password.Types (     Password,@@ -205,8 +205,7 @@     , encodeWithoutPadding key     ]   where-    encodeWithoutPadding bs =-        unsafeRemovePad64 (B.length bs) $ encodeBase64 bs+    encodeWithoutPadding = T.dropWhileEnd (== '=') . encodeBase64     parameters = T.intercalate ","         [ "m=" <> showT argon2MemoryCost         , "t=" <> showT argon2TimeCost@@ -274,14 +273,17 @@ -- -- prop> \(Blind badpass) -> let correctPasswordHash = hashPasswordWithSalt testParams salt "foobar" in checkPassword badpass correctPasswordHash == PasswordCheckFail checkPassword :: Password -> PasswordHash Argon2 -> PasswordCheck-checkPassword pass (PasswordHash passHash) =+checkPassword pass passHash =   fromMaybe PasswordCheckFail $ do-    let paramList = T.split (== '$') passHash-    (argon2Params, salt, hashedKey) <- parseArgon2Params paramList+    (argon2Params, salt, hashedKey) <- parseArgon2PasswordHashParams passHash     let producedKey = hashPasswordWithSalt' argon2Params salt pass     guard $ hashedKey `constEq` producedKey     return PasswordCheckSuccess +parseArgon2PasswordHashParams :: PasswordHash Argon2 -> Maybe (Argon2Params, Salt Argon2, ByteString)+parseArgon2PasswordHashParams (PasswordHash passHash) =+  parseArgon2Params $ T.split (== '$') passHash+ parseArgon2Params :: [Text] -> Maybe (Argon2Params, Salt Argon2, ByteString) -- vp - version or params -- ps - params or salt@@ -309,7 +311,7 @@     salt <- from64 $ unsafePad64 salt64     hashedKey <- from64 $ unsafePad64 hashedKey64     let argon2OutputLength = fromIntegral $ B.length hashedKey -- only here because of warnings-        argon2Salt = 16 -- only here because of warnings+        argon2Salt = fromIntegral $ B.length salt     pure (Argon2Params{..}, Salt salt, hashedKey)   where     parseParameters paramsT = do@@ -325,6 +327,20 @@             ("t=", i) -> go xs (m, readT i, p)             ("p=", i) -> go xs (m, t, readT i)             _ -> Nothing++-- | Extracts 'Argon2Params' from a 'PasswordHash' 'Argon2'.+--+-- Returns 'Just Argon2Params' on success.+--+-- >>> let pass = mkPassword "foobar"+-- >>> passHash <- hashPassword pass+-- >>> extractParams passHash == Just defaultParams+-- True+--+-- @since 3.0.2.0+extractParams :: PasswordHash Argon2 -> Maybe Argon2Params+extractParams passHash =+  (\(params, _, _) -> params) <$> parseArgon2PasswordHashParams passHash  -- | Strips the given 'match' if it matches and uses --   the function on the remainder of the given text.
src/Data/Password/Bcrypt.hs view
@@ -45,6 +45,7 @@   , PasswordCheck(..)   -- * Hashing Manually (bcrypt)   , hashPasswordWithParams+  , extractParams   -- ** Hashing with salt (DISADVISED)   --   -- | Hashing with a set 'Salt' is almost never what you want@@ -59,9 +60,12 @@     -- $setup   ) where +import Control.Monad (guard) import Control.Monad.IO.Class (MonadIO(liftIO)) import Crypto.KDF.BCrypt as Bcrypt (bcrypt, validatePassword) import Data.ByteArray (Bytes, convert)+import qualified Data.Text as T+import Text.Read (readMaybe)  import Data.Password.Types (     Password@@ -182,6 +186,22 @@         (toBytes passHash)       then PasswordCheckSuccess       else PasswordCheckFail++-- | Extracts the cost parameter as an 'Int' from a 'PasswordHash' 'Bcrypt'+--+-- >>> let pass = mkPassword "foobar"+-- >>> passHash <- hashPassword pass+-- >>> extractParams passHash == Just 10+-- True+--+-- @since 3.0.2.0+extractParams :: PasswordHash Bcrypt -> Maybe Int+extractParams (PasswordHash passHash) =+  case T.split (== '$') passHash of+    [_, version, cost, _pass] -> do+      guard $ elem version $ map T.pack ["2", "2a", "2x", "2y", "2b"]+      readMaybe $ T.unpack cost+    _ -> Nothing  -- | Generate a random 16-byte @bcrypt@ salt --
src/Data/Password/Internal.hs view
@@ -18,9 +18,10 @@   , fromBytes   , from64   , unsafePad64-  , unsafeRemovePad64   , readT   , showT+  , -- * Setup for doctests.+    -- $setup   ) where  import Control.Monad.IO.Class (MonadIO(liftIO))@@ -33,7 +34,6 @@ #endif import Data.Text as T (     Text,-    dropEnd,     length,     pack,     replicate,@@ -43,9 +43,26 @@ import Data.Text.Encoding (decodeUtf8, encodeUtf8) import Text.Read (readMaybe) +-- $setup+-- >>> import Data.ByteString as B (length)+-- >>> import Data.ByteString.Base64 (encodeBase64)+-- >>> import Data.Text as T (dropWhileEnd)+-- >>> import Data.Word (Word16)+-- >>> import Test.QuickCheck (ioProperty, quickCheck, (===))+-- >>> import Test.QuickCheck.Instances.ByteString()  -- | Generate a random x-byte-long salt. --+-- >>> :{+-- quickCheck $ \w ->+--   ioProperty $ do+--     let i :: Num a => a+--         i = fromIntegral (w :: Word16)+--     Salt bs <- newSalt i+--     pure $ B.length bs === i+-- :}+-- +++ OK, passed 100 tests.+-- -- @since 2.0.0.0 newSalt :: MonadIO m => Int -> m (Salt a) newSalt i = liftIO $ Salt <$> getRandomBytes i@@ -90,6 +107,8 @@ {-# INLINE showT #-}  -- | (UNSAFE) Pad a base64 text to "length `rem` 4 == 0" with "="+--+-- prop> \bs -> let b64 = encodeBase64 bs in unsafePad64 (T.dropWhileEnd (== '=') b64) == b64 unsafePad64 :: Text -> Text unsafePad64 t     | remains == 0 = t@@ -97,16 +116,3 @@   where     remains = T.length t `rem` 4     pad = T.replicate (4 - remains) "="---- | (UNSAFE) Removes the "=" padding from a base64 text--- given the length of the original bytestring.-unsafeRemovePad64 :: Int -> Text -> Text-unsafeRemovePad64 bsLen = T.dropEnd drops-  where-    drops = case bsLen `rem` 3 of-        -- 1 extra byte results in 2 characters (4 - 2 = 2)-        1 -> 2-        -- 2 extra bytes results in 3 characters (4 - 3 = 1)-        2 -> 1-        -- This will just be 0-        other -> other
src/Data/Password/PBKDF2.hs view
@@ -51,6 +51,7 @@   -- * Hashing Manually (PBKDF2)   , hashPasswordWithParams   , defaultParams+  , extractParams   , PBKDF2Params(..)   , PBKDF2Algorithm(..)   -- ** Hashing with salt (DISADVISED)@@ -233,8 +234,15 @@ -- -- prop> \(Blind badpass) -> let correctPasswordHash = hashPasswordWithSalt testParams salt "foobar" in checkPassword badpass correctPasswordHash == PasswordCheckFail checkPassword :: Password -> PasswordHash PBKDF2 -> PasswordCheck-checkPassword pass (PasswordHash passHash) =+checkPassword pass passHash =   fromMaybe PasswordCheckFail $ do+    (params, salt, hashedKey) <- parsePBKDF2PasswordHashParams passHash+    let producedKey = hashPasswordWithSalt' params salt pass+    guard $ hashedKey `constEq` producedKey+    return PasswordCheckSuccess++parsePBKDF2PasswordHashParams :: PasswordHash PBKDF2 -> Maybe (PBKDF2Params, Salt PBKDF2, ByteString)+parsePBKDF2PasswordHashParams (PasswordHash passHash) = do     -- This step makes it possible to also check the following format:     -- "pbkdf2:sha256:150000:etc.etc."     let passHash' = fromMaybe passHash $ "pbkdf2:" `T.stripPrefix` passHash@@ -249,11 +257,22 @@     salt <- from64 salt64     hashedKey <- from64 hashedKey64     let pbkdf2OutputLength = fromIntegral $ C8.length hashedKey-        producedKey = hashPasswordWithSalt' PBKDF2Params{..} (Salt salt) pass-    guard $ hashedKey `constEq` producedKey-    return PasswordCheckSuccess-  where-    pbkdf2Salt = 16+        pbkdf2Salt = fromIntegral $ C8.length salt+    return (PBKDF2Params{..}, Salt salt, hashedKey)++-- | Extracts 'PBKDF2Params' from a 'PasswordHash' 'PBKDF2'.+--+-- Returns 'Just PBKDF2Params' on success.+--+-- >>> let pass = mkPassword "foobar"+-- >>> passHash <- hashPassword pass+-- >>> extractParams passHash == Just defaultParams+-- True+--+-- @since 3.0.2.0+extractParams :: PasswordHash PBKDF2 -> Maybe PBKDF2Params+extractParams passHash =+  (\(params, _, _) -> params) <$> parsePBKDF2PasswordHashParams passHash   -- | Type of algorithm to use for hashing PBKDF2 passwords.
src/Data/Password/Scrypt.hs view
@@ -46,6 +46,7 @@   -- * Hashing Manually (scrypt)   , hashPasswordWithParams   , defaultParams+  , extractParams   , ScryptParams(..)   -- ** Hashing with salt (DISADVISED)   --@@ -243,8 +244,15 @@ -- -- prop> \(Blind badpass) -> let correctPasswordHash = hashPasswordWithSalt testParams salt "foobar" in checkPassword badpass correctPasswordHash == PasswordCheckFail checkPassword :: Password -> PasswordHash Scrypt -> PasswordCheck-checkPassword pass (PasswordHash passHash) =+checkPassword pass passHash =   fromMaybe PasswordCheckFail $ do+    (params, salt, hashedKey) <- parseScryptPasswordHashParams passHash+    let producedKey = hashPasswordWithSalt' params salt pass+    guard $ hashedKey `constEq` producedKey+    return PasswordCheckSuccess++parseScryptPasswordHashParams :: PasswordHash Scrypt -> Maybe (ScryptParams, Salt Scrypt, ByteString)+parseScryptPasswordHashParams (PasswordHash passHash) = do     let paramList = T.split (== '|') passHash     guard $ length paramList == 5     let [ scryptRoundsT,@@ -258,11 +266,22 @@     salt <- from64 salt64     hashedKey <- from64 hashedKey64     let scryptOutputLength = fromIntegral $ C8.length hashedKey-        producedKey = hashPasswordWithSalt' ScryptParams{..} (Salt salt) pass-    guard $ hashedKey `constEq` producedKey-    return PasswordCheckSuccess-  where-    scryptSalt = 32 -- only here because of warnings+        scryptSalt = fromIntegral $ C8.length salt+    return (ScryptParams{..}, Salt salt, hashedKey)++-- | Extracts 'ScryptParams' from a 'PasswordHash' 'Scrypt'.+--+-- Returns 'Just ScryptParams' on success.+--+-- >>> let pass = mkPassword "foobar"+-- >>> passHash <- hashPassword pass+-- >>> extractParams passHash == Just defaultParams+-- True+--+-- @since 3.0.2.0+extractParams :: PasswordHash Scrypt -> Maybe ScryptParams+extractParams passHash =+  (\(params, _, _) -> params) <$> parseScryptPasswordHashParams passHash  -- | Generate a random 32-byte @scrypt@ salt --
test/tasty/Argon2.hs view
@@ -12,25 +12,26 @@ testArgon2 :: TestTree testArgon2 = testGroup "Argon2"   [ referenceTest-  , testCorrectPassword "Argon2 (hashPassword)" hashFast checkPassword+  , testCorrectPassword "Argon2 (hashPassword, fast)" hashFast checkPassword extractParams fastParams+  , testCorrectPassword "Argon2 (hashPassword, slow)" hashSlow checkPassword extractParams slowParams   , testIncorrectPassword "Argon2 (hashPassword) fail" hashFast checkPassword   , testWithSalt "Argon2 (hashPasswordWithSalt)"                  (hashPasswordWithSalt fastParams)                  checkPassword-  , testWithParams "Argon2 (Argon2i)" $ fastParams{ argon2Variant = Argon2i }-  , testWithParams "Argon2 (Argon2d)" $ fastParams{ argon2Variant = Argon2d }+                 extractParams+                 fastParams+  , testWithParams "Argon2 (Argon2i)" (fastParams{ argon2Variant = Argon2i })+  , testWithParams "Argon2 (Argon2d)" (fastParams{ argon2Variant = Argon2d })   , paddingTests   , omittedVersionTest   ]   where     testWithParams s params =-      testWithSalt s (hashPasswordWithSalt params) checkPassword+      testWithSalt s (hashPasswordWithSalt params) checkPassword extractParams params     hashFast = hashPasswordWithParams fastParams-    fastParams =-      defaultParams{-        argon2MemoryCost = 2 ^ (8 :: Int),-        argon2TimeCost = 1-      }+    fastParams = defaultParams{ argon2MemoryCost = 2 ^ (8 :: Int), argon2TimeCost = 1 }+    hashSlow = hashPasswordWithParams slowParams+    slowParams = defaultParams{ argon2MemoryCost = 2 ^ (8 :: Int), argon2TimeCost = 4 }  paddingTests :: TestTree paddingTests = testGroup "Padding"
test/tasty/Bcrypt.hs view
@@ -4,12 +4,13 @@  import Data.Password.Bcrypt -import Internal (testCorrectPassword, testIncorrectPassword, testWithSalt)+import Internal   testBcrypt :: TestTree testBcrypt = testGroup "bcrypt"-  [ testCorrectPassword "Bcrypt (hashPassword)" (hashPasswordWithParams 4) checkPassword+  [ testCorrectPassword "Bcrypt (hashPassword)" (hashPasswordWithParams 4) checkPassword extractParams 4+  , testCorrectPassword "Bcrypt (hashPassword 2)" (hashPasswordWithParams 5) checkPassword extractParams 5   , testIncorrectPassword "Bcrypt (hashPassword) fail" (hashPasswordWithParams 4) checkPassword-  , testWithSalt "Bcrypt (hashPasswordWithSalt)" (hashPasswordWithSalt 4) checkPassword+  , testWithSalt "Bcrypt (hashPasswordWithSalt)" (hashPasswordWithSalt 4) checkPassword extractParams 4   ]
test/tasty/Internal.hs view
@@ -11,15 +11,18 @@ import Data.Password.Bcrypt (PasswordCheck(..), Salt(..))  -testCorrectPassword :: String+testCorrectPassword :: (Eq params, Show params)+                    => String                     -> (Password -> IO (PasswordHash a))                     -> (Password -> PasswordHash a -> PasswordCheck)+                    -> (PasswordHash a -> Maybe params)+                    -> params                     -> TestTree-testCorrectPassword s hashF checkF = testProperty s $+testCorrectPassword s hashF checkF extractParamsF params = testProperty s $   \pass -> ioProperty $ do     let pw = mkPassword pass     hpw <- hashF pw-    return $ checkF pw hpw === PasswordCheckSuccess+    return $ (checkF pw hpw === PasswordCheckSuccess) .&&. extractParamsF hpw === Just params  testIncorrectPassword :: String                       -> (Password -> IO (PasswordHash a))@@ -35,15 +38,18 @@   where     isEmpty c = c `elem` ["", "\NUL"] -testWithSalt :: String+testWithSalt :: (Eq params, Show params)+             => String              -> (Salt a -> Password -> PasswordHash a)              -> (Password -> PasswordHash a -> PasswordCheck)+             -> (PasswordHash a -> Maybe params)+             -> params              -> TestTree-testWithSalt s hashWithSalt checkF = testProperty s $+testWithSalt s hashWithSalt checkF extractParamsF params = testProperty s $   \pass salt ->     let pw = mkPassword pass         hpw = hashWithSalt salt pw-    in checkF pw hpw === PasswordCheckSuccess+    in (checkF pw hpw === PasswordCheckSuccess) .&&. extractParamsF hpw === Just params  instance Arbitrary (Salt a) where   arbitrary = Salt . pack <$> vector 16
test/tasty/PBKDF2.hs view
@@ -28,9 +28,11 @@       "PBKDF2 (hashPasswordWithSalt)"       (hashPasswordWithSalt testParams)       checkPassword-  , testIt "PBKDF2 (md5)"    (defaultParams{ pbkdf2Algorithm = PBKDF2_MD5, pbkdf2Iterations = 1000 })-  , testIt "PBKDF2 (sha1)"   (testParams{ pbkdf2Algorithm = PBKDF2_SHA1 })-  , testIt "PBKDF2 (sha256)" (testParams{ pbkdf2Algorithm = PBKDF2_SHA256 })+      extractParams+      testParams+  , testIt "PBKDF2 (md5)"    (defaultParams{ pbkdf2Algorithm = PBKDF2_MD5, pbkdf2Iterations = 1000, pbkdf2OutputLength = 16 })+  , testIt "PBKDF2 (sha1)"   (testParams{ pbkdf2Algorithm = PBKDF2_SHA1, pbkdf2OutputLength = 20 })+  , testIt "PBKDF2 (sha256)" (testParams{ pbkdf2Algorithm = PBKDF2_SHA256, pbkdf2OutputLength = 32 })   , testFast Crypto.SHA1   20 PBKDF2.fastPBKDF2_SHA1   , testFast Crypto.SHA256 32 PBKDF2.fastPBKDF2_SHA256   , testFast Crypto.SHA512 64 PBKDF2.fastPBKDF2_SHA512@@ -39,9 +41,11 @@       "PBKDF2 (pbkdf2:sha-...)"       (hashPasswordWithParams testParams)       (\pass (PasswordHash hash) -> checkPassword pass . PasswordHash $ "pbkdf2:" <> hash)+      extractParams+      testParams   ]   where-    testIt s params = testCorrectPassword s (hashPasswordWithParams params) checkPassword+    testIt s params = testCorrectPassword s (hashPasswordWithParams params) checkPassword extractParams params     testParams = defaultParams{ pbkdf2Iterations = 4 * 1000 }  testFast :: (HashAlgorithm a, Show a)
test/tasty/Scrypt.hs view
@@ -16,15 +16,21 @@  testScrypt :: TestTree testScrypt = testGroup "scrypt"-  [ testCorrectPassword "Scrypt (hashPassword)" hash8Rounds checkPassword+  [ testCorrectPassword "Scrypt (hashPassword, 8 rounds)" hash8Rounds checkPassword extractParams testsParams8Rounds+  , testCorrectPassword "Scrypt (hashPassword, 4 rounds)" hash4Rounds checkPassword extractParams testsParams4Rounds   , testIncorrectPassword "Scrypt (hashPassword) fail" hash8Rounds checkPassword   , testWithSalt "Scrypt (hashPasswordWithSalt)"-                 (hashPasswordWithSalt defaultParams{ scryptRounds = 8 })+                 (hashPasswordWithSalt testsParams8Rounds)                  checkPassword+                 extractParams+                 testsParams8Rounds   , testProperty "scrypt <-> cryptonite" $ withMaxSuccess 10 checkScrypt   ]   where-    hash8Rounds = hashPasswordWithParams defaultParams{ scryptRounds = 8 }+    hash8Rounds = hashPasswordWithParams testsParams8Rounds+    testsParams8Rounds = defaultParams{ scryptRounds = 8, scryptSalt = 16 }+    hash4Rounds = hashPasswordWithParams testsParams4Rounds+    testsParams4Rounds = defaultParams{ scryptRounds = 4, scryptSalt = 16 }  checkScrypt :: Text -> Property checkScrypt pass = ioProperty $ do