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 +6/−0
- password.cabal +4/−4
- src/Data/Password/Argon2.hs +24/−8
- src/Data/Password/Bcrypt.hs +20/−0
- src/Data/Password/Internal.hs +21/−15
- src/Data/Password/PBKDF2.hs +25/−6
- src/Data/Password/Scrypt.hs +25/−6
- test/tasty/Argon2.hs +10/−9
- test/tasty/Bcrypt.hs +4/−3
- test/tasty/Internal.hs +12/−6
- test/tasty/PBKDF2.hs +8/−4
- test/tasty/Scrypt.hs +9/−3
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