Add BCrypt module doc and a validatePasswordEither fn
This commit is contained in:
parent
a888501bb8
commit
f346c46243
@ -1,7 +1,29 @@
|
|||||||
|
|
||||||
|
-- | Password encoding and validation using bcrypt.
|
||||||
|
--
|
||||||
|
-- See <https://www.usenix.org/conference/1999-usenix-annual-technical-conference/future-adaptable-password-scheme>
|
||||||
|
-- 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
|
module Crypto.KDF.BCrypt
|
||||||
( hashPassword
|
( hashPassword
|
||||||
, validatePassword
|
, validatePassword
|
||||||
|
, validatePasswordEither
|
||||||
, bcrypt
|
, bcrypt
|
||||||
)
|
)
|
||||||
where
|
where
|
||||||
@ -25,7 +47,7 @@ hashPassword :: (MonadRandom m, ByteArray password, ByteArray hash)
|
|||||||
-- ^ The cost parameter. Should be between 4 and 31 (inclusive).
|
-- ^ The cost parameter. Should be between 4 and 31 (inclusive).
|
||||||
-- Values which lie outside this range will be adjusted accordingly.
|
-- Values which lie outside this range will be adjusted accordingly.
|
||||||
-> password
|
-> password
|
||||||
-- ^ The password.
|
-- ^ The password. Should be the UTF-8 encoded bytes of the password text.
|
||||||
-> m hash
|
-> m hash
|
||||||
-- ^ The bcrypt hash in standard format.
|
-- ^ The bcrypt hash in standard format.
|
||||||
hashPassword cost password = do
|
hashPassword cost password = do
|
||||||
@ -40,7 +62,7 @@ bcrypt :: (ByteArray salt, ByteArray password, ByteArray output)
|
|||||||
-> salt
|
-> salt
|
||||||
-- ^ The salt. Must be 16 bytes in length or an error will be raised.
|
-- ^ The salt. Must be 16 bytes in length or an error will be raised.
|
||||||
-> password
|
-> password
|
||||||
-- ^ The password.
|
-- ^ The password. Should be the UTF-8 encoded bytes of the password text.
|
||||||
-> output
|
-> output
|
||||||
-- ^ The bcrypt hash in standard format.
|
-- ^ The bcrypt hash in standard format.
|
||||||
bcrypt cost salt password = B.concat [header, B.snoc costBytes dollar, b64 salt, b64 hash]
|
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')
|
zero = fromIntegral (ord '0')
|
||||||
costBytes = B.pack [zero + fromIntegral (realCost `div` 10), zero + fromIntegral (realCost `mod` 10)]
|
costBytes = B.pack [zero + fromIntegral (realCost `div` 10), zero + fromIntegral (realCost `mod` 10)]
|
||||||
realCost
|
realCost
|
||||||
| cost < 4 = 4
|
| cost < 4 = 10 -- 4 is virtually pointless so go for 10
|
||||||
| cost > 31 = 31
|
| cost > 31 = 31
|
||||||
| otherwise = cost
|
| 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
|
-- Returns @False@ if the password doesn't match the hash, or if the hash is
|
||||||
-- invalid or an unsupported version.
|
-- invalid or an unsupported version.
|
||||||
validatePassword :: (ByteArray password, ByteArray hash) => password -> hash -> Bool
|
validatePassword :: (ByteArray password, ByteArray hash) => password -> hash -> Bool
|
||||||
validatePassword password bcHash = case parseBCryptHash bcHash of
|
validatePassword password bcHash = either (const False) id (validatePasswordEither password bcHash)
|
||||||
Right (BCH version cost salt hash) -> (rawHash version cost salt password :: Bytes) `B.constEq` hash
|
|
||||||
Left _ -> False
|
-- | 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 :: (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
|
where
|
||||||
hash = loop (0 :: Int) orpheanBeholder
|
hash = loop (0 :: Int) orpheanBeholder
|
||||||
|
|
||||||
@ -87,11 +116,12 @@ rawHash version cost salt password = B.take 23 hash
|
|||||||
-- "$2a$10$XajjQvNhvvRt5GSeFk1xFeyqRrsxkhBkUiQeg0dt.wU1qD4aFDcga"
|
-- "$2a$10$XajjQvNhvvRt5GSeFk1xFeyqRrsxkhBkUiQeg0dt.wU1qD4aFDcga"
|
||||||
parseBCryptHash :: (ByteArray ba) => ba -> Either String BCryptHash
|
parseBCryptHash :: (ByteArray ba) => ba -> Either String BCryptHash
|
||||||
parseBCryptHash bc = do
|
parseBCryptHash bc = do
|
||||||
unless (B.length bc == 60 &&
|
unless (B.length bc == 60 &&
|
||||||
B.index bc 0 == dollar &&
|
B.index bc 0 == dollar &&
|
||||||
B.index bc 1 == fromIntegral (ord '2') &&
|
B.index bc 1 == fromIntegral (ord '2') &&
|
||||||
B.index bc 3 == dollar &&
|
B.index bc 3 == dollar &&
|
||||||
B.index bc 6 == dollar) (Left "Invalid hash format")
|
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")
|
when (costTens > 3 || cost > 31 || cost < 4) (Left "Invalid bcrypt cost")
|
||||||
(salt, hash) <- decodeSaltHash (B.drop 7 bc)
|
(salt, hash) <- decodeSaltHash (B.drop 7 bc)
|
||||||
return (BCH version cost salt hash)
|
return (BCH version cost salt hash)
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user