password-2.0.1.1: src/Data/Password/Bcrypt.hs
{-|
Module : Data.Password.Bcrypt
Copyright : (c) Felix Paulusma, 2020
License : BSD-style (see LICENSE file)
Maintainer : cdep.illabout@gmail.com
Stability : experimental
Portability : POSIX
= bcrypt
The @bcrypt@ algorithm is a popular way of hashing passwords.
It is based on the Blowfish cipher and fairly straightfoward
in its usage. It has a cost parameter that, when increased,
slows down the hashing speed.
It is a straightforward and easy way to get decent protection
on passwords, it is also been around long enough to be battle-tested
and generally considered to provide a good amount of security.
== Other algorithms
@bcrypt@, together with @'Data.Password.PBKDF2.PBKDF2'@, are only computationally intensive.
And to protect from specialized hardware, new algorithms have been
developed that are also resource intensive, like @'Data.Password.Scrypt.Scrypt'@ and
@'Data.Password.Argon2.Argon2'@. Not having high resource demands, means an attacker with
specialized software could take less time to brute-force a password,
though with the default cost (10) and a decently long password,
the amount of time to brute-force would still be significant.
This the algorithm to use if you're not sure about your needs, but
just want a decent, proven way to encrypt your passwords.
-}
module Data.Password.Bcrypt (
-- * Algorithm
Bcrypt
-- * Plain-text Password
, Password
, mkPassword
-- * Hash Passwords (bcrypt)
, hashPassword
, PasswordHash(..)
-- * Verify Passwords (bcrypt)
, checkPassword
, PasswordCheck(..)
-- * Hashing Manually (bcrypt)
, hashPasswordWithParams
-- ** Hashing with salt (DISADVISED)
--
-- | Hashing with a set 'Salt' is almost never what you want
-- to do. Use 'hashPassword' or 'hashPasswordWithParams' to have
-- automatic generation of randomized salts.
, hashPasswordWithSalt
, newSalt
, Salt(..)
-- * Unsafe debugging function to show a Password
, unsafeShowPassword
, -- * Setup for doctests.
-- $setup
) where
import Control.Monad.IO.Class (MonadIO(liftIO))
import Crypto.KDF.BCrypt as Bcrypt hiding (hashPassword)
import Data.ByteArray (Bytes, convert)
import Data.Password (
PasswordCheck(..)
, PasswordHash(..)
, Salt(..)
, mkPassword
, unsafeShowPassword
)
import Data.Password.Internal (Password(..), fromBytes, toBytes)
import qualified Data.Password.Internal (newSalt)
-- | Phantom type for __bcrypt__
--
-- @since 2.0.0.0
data Bcrypt
-- $setup
-- >>> :set -XFlexibleInstances
-- >>> :set -XOverloadedStrings
--
-- Import needed libraries.
--
-- >>> import Data.Password
-- >>> import Data.ByteString (pack)
-- >>> import Test.QuickCheck (Arbitrary(arbitrary), Blind(Blind), vector)
-- >>> import Test.QuickCheck.Instances.Text ()
--
-- >>> instance Arbitrary (Salt a) where arbitrary = Salt . pack <$> vector 16
-- >>> instance Arbitrary Password where arbitrary = fmap Password arbitrary
-- >>> let salt = Salt "abcdefghijklmnop"
-- -- >>> instance Arbitrary (PasswordHash Bcrypt) where arbitrary = hashPasswordWithSalt 8 <$> arbitrary <*> arbitrary
-- | Hash the 'Password' using the /bcrypt/ hash algorithm.
--
-- __N.B.__: @bcrypt@ has a limit of 72 bytes as input, so anything longer than that
-- will be cut off at the 72 byte point and thus any password that is 72 bytes
-- or longer will match as long as the first 72 bytes are the same.
--
-- >>> hashPassword $ mkPassword "foobar"
-- PasswordHash {unPasswordHash = "$2b$10$..."}
hashPassword :: MonadIO m => Password -> m (PasswordHash Bcrypt)
hashPassword = hashPasswordWithParams 10
-- | Hash a password with the given cost and also with the given 'Salt'
-- instead of generating a random salt. Using 'hashPasswordWithSalt' is strongly __disadvised__,
-- and 'hashPasswordWithParams' should be used instead. /Never use a static salt/
-- /in production applications!/
--
-- __N.B.__: The salt HAS to be 16 bytes or this function will throw an error!
--
-- >>> let salt = Salt "abcdefghijklmnop"
-- >>> hashPasswordWithSalt 10 salt (mkPassword "foobar")
-- PasswordHash {unPasswordHash = "$2b$10$WUHhXETkX0fnYkrqZU3ta.N8Utt4U77kW4RVbchzgvBvBBEEdCD/u"}
--
-- (Note that we use an explicit 'Salt' in the example above. This is so that the
-- example is reproducible, but in general you should use 'hashPassword'. 'hashPassword'
-- (and 'hashPasswordWithParams') generates a new 'Salt' everytime it is called.)
hashPasswordWithSalt
:: Int -- ^ The cost parameter. Should be between 4 and 31 (inclusive). Values which lie outside this range will be adjusted accordingly.
-> Salt Bcrypt -- ^ The salt. MUST be 16 bytes in length or an error will be raised.
-> Password -- ^ The password to be hashed.
-> PasswordHash Bcrypt -- ^ The bcrypt hash in standard format.
hashPasswordWithSalt cost (Salt salt) (Password pass) =
let hash = Bcrypt.bcrypt cost (convert salt :: Bytes) (toBytes pass)
in PasswordHash $ fromBytes hash
-- | Hash a password using the /bcrypt/ algorithm with the given cost.
--
-- The higher the cost, the longer 'hashPassword' and 'checkPassword' will take to run,
-- thus increasing the security, but taking longer and taking up more resources.
-- The optimal cost for generic user logins would be one that would take between
-- 0.05 - 0.5 seconds to check on the machine that will run it.
--
-- __N.B.__: It is advised to use 'hashPassword' if you're unsure about the
-- implications that changing the cost brings with it.
--
-- @since 2.0.0.0
hashPasswordWithParams
:: MonadIO m
=> Int -- ^ The cost parameter. Should be between 4 and 31 (inclusive). Values which lie outside this range will be adjusted accordingly.
-> Password -- ^ The password to be hashed.
-> m (PasswordHash Bcrypt) -- ^ The bcrypt hash in standard format.
hashPasswordWithParams cost pass = liftIO $ do
salt <- newSalt
return $ hashPasswordWithSalt cost salt pass
-- | Check a 'Password' against a 'PasswordHash' 'Bcrypt'.
--
-- Returns 'PasswordCheckSuccess' on success.
--
-- >>> let pass = mkPassword "foobar"
-- >>> passHash <- hashPassword pass
-- >>> checkPassword pass passHash
-- PasswordCheckSuccess
--
-- Returns 'PasswordCheckFail' if an incorrect 'Password' or 'PasswordHash' 'Bcrypt' is used.
--
-- >>> let badpass = mkPassword "incorrect-password"
-- >>> checkPassword badpass passHash
-- PasswordCheckFail
--
-- This should always fail if an incorrect password is given.
--
-- prop> \(Blind badpass) -> let correctPasswordHash = hashPasswordWithSalt 8 salt "foobar" in checkPassword badpass correctPasswordHash == PasswordCheckFail
checkPassword :: Password -> PasswordHash Bcrypt -> PasswordCheck
checkPassword (Password pass) (PasswordHash passHash) =
if Bcrypt.validatePassword (toBytes pass) (toBytes passHash)
then PasswordCheckSuccess
else PasswordCheckFail
-- | Generate a random 16-byte @bcrypt@ salt
--
-- @since 2.0.0.0
newSalt :: MonadIO m => m (Salt Bcrypt)
newSalt = Data.Password.Internal.newSalt 16