password 3.0.0.0 → 3.0.1.0
raw patch · 4 files changed
+77/−32 lines, 4 filesdep ~bytestringdep ~cryptonitedep ~memoryPVP ok
version bump matches the API change (PVP)
Dependency ranges changed: bytestring, cryptonite, memory
API changes (from Hackage documentation)
Files
- ChangeLog.md +6/−0
- password.cabal +3/−3
- src/Data/Password/Argon2.hs +41/−25
- test/tasty/Argon2.hs +27/−4
ChangeLog.md view
@@ -1,5 +1,11 @@ # Changelog for `password` +## 3.0.1.0++- Argon2 hashes without a version field are interpreted as being of version 1.0+ Thanks to [@Vlix](https://github.com/Vlix)+ [#56](https://github.com/cdepillabout/password/pull/56)+ ## 3.0.0.0 - Split the main datatypes module (`Data.Password`) into a separate package: `password-types`.
password.cabal view
@@ -1,7 +1,7 @@ cabal-version: 1.12 name: password-version: 3.0.0.0+version: 3.0.1.0 category: Data synopsis: Hashing and checking of passwords description:@@ -72,8 +72,8 @@ base >= 4.9 && < 5 , base64 >= 0.3 && < 0.5 , bytestring >= 0.10.8.1 && < 0.11- , cryptonite >= 0.15.1 && < 0.29- , memory >= 0.14 && < 0.16+ , cryptonite >= 0.15.1 && < 0.30+ , memory >= 0.14 && < 0.17 , password-types < 2 , template-haskell , text >= 1.2.2 && < 1.3
src/Data/Password/Argon2.hs view
@@ -81,7 +81,7 @@ import Data.Semigroup ((<>)) #endif import Data.Text (Text)-import qualified Data.Text as T (intercalate, length, split, splitAt)+import qualified Data.Text as T (intercalate, split, splitAt, stripPrefix) import Data.Word (Word32) import Data.Password.Internal (@@ -277,30 +277,45 @@ checkPassword pass (PasswordHash passHash) = fromMaybe PasswordCheckFail $ do let paramList = T.split (== '$') passHash- guard $ Prelude.length paramList == 6- let [ _,- variantT,- versionT,- parametersT,- salt64,- hashedKey64 ] = paramList- argon2Variant <- parseVariant variantT- argon2Version <- parseVersion versionT- (argon2MemoryCost, argon2TimeCost, argon2Parallelism) <- parseParameters parametersT- salt <- from64 $ unsafePad64 salt64- hashedKey <- from64 $ unsafePad64 hashedKey64- let argon2OutputLength = fromIntegral $ B.length hashedKey -- only here because of warnings- producedKey = hashPasswordWithSalt' Argon2Params{..} (Salt salt) pass+ (argon2Params, salt, hashedKey) <- parseArgon2Params paramList+ let producedKey = hashPasswordWithSalt' argon2Params salt pass guard $ hashedKey `constEq` producedKey return PasswordCheckSuccess++parseArgon2Params :: [Text] -> Maybe (Argon2Params, Salt Argon2, ByteString)+-- vp - version or params+-- ps - params or salt+-- sh - salt or hash+parseArgon2Params (_:variantT:vp:ps:sh:rest) = do+ variant <- parseVariant variantT+ case rest of+ -- If there is a 6th part, we'll assume the version is included+ [hashedKey64] -> do+ version <- parseVersion vp+ parseAll variant version ps sh hashedKey64+ -- If there are only 5 parts, we'll assume the version is 'Version10'+ [] -> parseAll variant Version10 vp ps sh+ -- Any other amount of parts means the provided hash is malformed+ _ -> Nothing where- argon2Salt = 16 -- only here because of warnings parseVariant = splitMaybe "argon2" letterToVariant parseVersion = splitMaybe "v=" numToVersion- parseParameters params = do- let ps = T.split (== ',') params- guard $ Prelude.length ps == 3- go ps (Nothing, Nothing, Nothing)+-- If there are less than 5 parts, the hash is malformed+parseArgon2Params _ = Nothing++parseAll :: Argon2.Variant -> Argon2.Version -> Text -> Text -> Text -> Maybe (Argon2Params, Salt Argon2, ByteString)+parseAll argon2Variant argon2Version parametersT salt64 hashedKey64 = do+ (argon2MemoryCost, argon2TimeCost, argon2Parallelism) <- parseParameters parametersT+ 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+ pure (Argon2Params{..}, Salt salt, hashedKey)+ where+ parseParameters paramsT = do+ let paramsL = T.split (== ',') paramsT+ guard $ Prelude.length paramsL == 3+ go paramsL (Nothing, Nothing, Nothing) where go [] (Just m, Just t, Just p) = Just (m, t, p) go [] _ = Nothing@@ -310,11 +325,12 @@ ("t=", i) -> go xs (m, readT i, p) ("p=", i) -> go xs (m, t, readT i) _ -> Nothing- splitMaybe :: Text -> (Text -> Maybe a) -> Text -> Maybe a- splitMaybe match f t =- case T.splitAt (T.length match) t of- (m, x) | m == match -> f x- _ -> Nothing++-- | Strips the given 'match' if it matches and uses+-- the function on the remainder of the given text.+splitMaybe :: Text -> (Text -> Maybe a) -> Text -> Maybe a+splitMaybe match f t =+ T.stripPrefix match t >>= f -- | Generate a random 16-byte @Argon2@ salt --
test/tasty/Argon2.hs view
@@ -1,7 +1,7 @@ {-# LANGUAGE OverloadedStrings #-}-module Argon2 where+module Argon2 (testArgon2) where -import Test.Tasty+import Test.Tasty (TestTree, testGroup) import Test.Tasty.HUnit (assertBool, assertEqual, testCase) import Data.Password.Argon2@@ -20,6 +20,7 @@ , testWithParams "Argon2 (Argon2i)" $ fastParams{ argon2Variant = Argon2i } , testWithParams "Argon2 (Argon2d)" $ fastParams{ argon2Variant = Argon2d } , paddingTests+ , omittedVersionTest ] where testWithParams s params =@@ -47,14 +48,28 @@ hashWithPadding = PasswordHash "$argon2id$v=19$m=65536,t=2,p=1$YWJjZGVmZ2hpamtsbW5vcA==$BztdyfEefG5V18ZNlztPrfZaU5duVFKZiI6dJeWht0o=" hashWithoutPadding = PasswordHash "$argon2id$v=19$m=65536,t=2,p=1$YWJjZGVmZ2hpamtsbW5vcA$BztdyfEefG5V18ZNlztPrfZaU5duVFKZiI6dJeWht0o" +-- Very old hashes might not have version parts, so infer as version 1.0+omittedVersionTest :: TestTree+omittedVersionTest = testGroup "Version 1.0"+ [ go "version 1.0 part in hash (placebo)" "testtest" v10Hash+ , go "no version part in hash == version 1.0" "testtest" v10HashWithoutVersion+ , go "version 1.3 part in hash (reference)" "password" referenceHash+ , testCase "no version 1.3 part in hash should fail" $+ assertEqual "check passed!?" PasswordCheckFail $+ checkPassword "password" referenceHashWithoutVersion+ ]+ where+ go s p = testCase s+ . assertEqual "check failed" PasswordCheckSuccess+ . checkPassword p+ -- Reference check using the Command-line Utility output example -- from: https://github.com/P-H-C/phc-winner-argon2 referenceTest :: TestTree referenceTest = testCase "PHC Argon2 reference" $- assertEqual "output hash is wrong" expected $+ assertEqual "output hash is wrong" referenceHash $ hashPasswordWithSalt params salt pwd where- expected = PasswordHash "$argon2i$v=19$m=65536,t=2,p=4$c29tZXNhbHQ$RdescudvJCsgt3ub+b+dWRWJTmaaJObG" salt = Salt "somesalt" pwd = mkPassword "password" params = defaultParams {@@ -62,3 +77,11 @@ argon2Parallelism = 4, argon2OutputLength = 24 }++-- Weirdly lined out to show it's exactly the same, except the 'v=' part is missing.+referenceHash, referenceHashWithoutVersion :: PasswordHash Argon2+referenceHash = PasswordHash "$argon2i$v=19$m=65536,t=2,p=4$c29tZXNhbHQ$RdescudvJCsgt3ub+b+dWRWJTmaaJObG"+referenceHashWithoutVersion = PasswordHash "$argon2i$m=65536,t=2,p=4$c29tZXNhbHQ$RdescudvJCsgt3ub+b+dWRWJTmaaJObG"+v10Hash, v10HashWithoutVersion :: PasswordHash Argon2+v10Hash = PasswordHash "$argon2i$v=16$m=65536,t=2,p=1$Kx1BEcpIg0Ey5GyXq5do2w$0qRfWHw09EdqQkSsaG57O/ou8v/E6Vc83w"+v10HashWithoutVersion = PasswordHash "$argon2i$m=65536,t=2,p=1$Kx1BEcpIg0Ey5GyXq5do2w$0qRfWHw09EdqQkSsaG57O/ou8v/E6Vc83w"