From f346c462432c1b49ad9150bf7ac8c5ca28c15f31 Mon Sep 17 00:00:00 2001 From: Luke Taylor Date: Sat, 15 Aug 2015 17:12:08 +0100 Subject: [PATCH] Add BCrypt module doc and a validatePasswordEither fn --- Crypto/KDF/BCrypt.hs | 46 ++++++++++++++++++++++++++++++++++++-------- 1 file changed, 38 insertions(+), 8 deletions(-) diff --git a/Crypto/KDF/BCrypt.hs b/Crypto/KDF/BCrypt.hs index 516747e..700f41e 100644 --- a/Crypto/KDF/BCrypt.hs +++ b/Crypto/KDF/BCrypt.hs @@ -1,7 +1,29 @@ +-- | Password encoding and validation using bcrypt. +-- +-- See +-- for details of the original algorithm. +-- +-- Hashes are strings of the form @$2a$10$MJJifxfaqQmbx1Mhsq3oq.YmMmfNhkyW4s/MS3K5rIMVfB7w0Q/OW@ which +-- encode a version number, an integer cost parameter and the concatenated salt and hash bytes (each +-- separately Base64 encoded. Incrementing the cost parameter approximately doubles the time taken +-- to calculate the hash. +-- +-- The different version numbers have evolved because of bugs in the standard C implementations. +-- The most up to date version is @2b@ and this implementation the @2b@ version prefix, but will also +-- attempt to validate against hashes with versions @2a@ and @2y@. Version @2@ or @2x@ will be rejected. +-- No attempt is made to differentiate between the different versions when validating a password, but +-- in practice this shouldn't cause any problems if passwords are UTF-8 encoded (which they should be). +-- +-- The cost parameter can be between 4 and 31 inclusive, but anything less than 10 is probably not strong +-- enough. High values may be prohibitively slow depending on your hardware. Choose the highest value you +-- can without having an unacceptable impact on your users. The cost parameter can also varied depending on +-- the account, since it is unique to an individual hash. + module Crypto.KDF.BCrypt ( hashPassword , validatePassword + , validatePasswordEither , bcrypt ) where @@ -25,7 +47,7 @@ hashPassword :: (MonadRandom m, ByteArray password, ByteArray hash) -- ^ The cost parameter. Should be between 4 and 31 (inclusive). -- Values which lie outside this range will be adjusted accordingly. -> password - -- ^ The password. + -- ^ The password. Should be the UTF-8 encoded bytes of the password text. -> m hash -- ^ The bcrypt hash in standard format. hashPassword cost password = do @@ -40,7 +62,7 @@ bcrypt :: (ByteArray salt, ByteArray password, ByteArray output) -> salt -- ^ The salt. Must be 16 bytes in length or an error will be raised. -> password - -- ^ The password. + -- ^ The password. Should be the UTF-8 encoded bytes of the password text. -> output -- ^ The bcrypt hash in standard format. bcrypt cost salt password = B.concat [header, B.snoc costBytes dollar, b64 salt, b64 hash] @@ -51,7 +73,7 @@ bcrypt cost salt password = B.concat [header, B.snoc costBytes dollar, b64 salt, zero = fromIntegral (ord '0') costBytes = B.pack [zero + fromIntegral (realCost `div` 10), zero + fromIntegral (realCost `mod` 10)] realCost - | cost < 4 = 4 + | cost < 4 = 10 -- 4 is virtually pointless so go for 10 | cost > 31 = 31 | otherwise = cost @@ -63,12 +85,19 @@ bcrypt cost salt password = B.concat [header, B.snoc costBytes dollar, b64 salt, -- Returns @False@ if the password doesn't match the hash, or if the hash is -- invalid or an unsupported version. validatePassword :: (ByteArray password, ByteArray hash) => password -> hash -> Bool -validatePassword password bcHash = case parseBCryptHash bcHash of - Right (BCH version cost salt hash) -> (rawHash version cost salt password :: Bytes) `B.constEq` hash - Left _ -> False +validatePassword password bcHash = either (const False) id (validatePasswordEither password bcHash) + +-- | Check a password against a bcrypt hash +-- +-- As for @validatePassword@ but will provide error information if the hash is invalid or +-- an unsupported version. +validatePasswordEither :: (ByteArray password, ByteArray hash) => password -> hash -> Either String Bool +validatePasswordEither password bcHash = do + BCH version cost salt hash <- parseBCryptHash bcHash + return $ (rawHash version cost salt password :: Bytes) `B.constEq` hash rawHash :: (ByteArrayAccess salt, ByteArray password, ByteArray output) => Char -> Int -> salt -> password -> output -rawHash version cost salt password = B.take 23 hash +rawHash _ cost salt password = B.take 23 hash -- Another compatibility bug. Ignore last byte of hash where hash = loop (0 :: Int) orpheanBeholder @@ -87,11 +116,12 @@ rawHash version cost salt password = B.take 23 hash -- "$2a$10$XajjQvNhvvRt5GSeFk1xFeyqRrsxkhBkUiQeg0dt.wU1qD4aFDcga" parseBCryptHash :: (ByteArray ba) => ba -> Either String BCryptHash parseBCryptHash bc = do - unless (B.length bc == 60 && + unless (B.length bc == 60 && B.index bc 0 == dollar && B.index bc 1 == fromIntegral (ord '2') && B.index bc 3 == dollar && B.index bc 6 == dollar) (Left "Invalid hash format") + unless (version == 'b' || version == 'a' || version == 'y') (Left ("Unsupported minor version: " ++ [version])) when (costTens > 3 || cost > 31 || cost < 4) (Left "Invalid bcrypt cost") (salt, hash) <- decodeSaltHash (B.drop 7 bc) return (BCH version cost salt hash)