{-| 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 has 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 , extractParams -- ** 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 (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 , PasswordHash(..) , mkPassword , unsafeShowPassword , Salt(..) ) import Data.Password.Internal ( PasswordCheck(..) , 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.Types -- >>> 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 mkPassword 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 :: forall (m :: * -> *). MonadIO m => Password -> m (PasswordHash Bcrypt) hashPassword = forall (m :: * -> *). MonadIO m => Int -> Password -> m (PasswordHash Bcrypt) hashPasswordWithParams Int 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 :: Int -> Salt Bcrypt -> Password -> PasswordHash Bcrypt hashPasswordWithSalt Int cost (Salt ByteString salt) Password pass = let hash :: Bytes hash = forall salt password output. (ByteArray salt, ByteArray password, ByteArray output) => Int -> salt -> password -> output Bcrypt.bcrypt Int cost (forall bin bout. (ByteArrayAccess bin, ByteArray bout) => bin -> bout convert ByteString salt :: Bytes) (Text -> Bytes toBytes forall a b. (a -> b) -> a -> b $ Password -> Text unsafeShowPassword Password pass) in forall a. Text -> PasswordHash a PasswordHash forall a b. (a -> b) -> a -> b $ Bytes -> Text fromBytes Bytes 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 :: forall (m :: * -> *). MonadIO m => Int -> Password -> m (PasswordHash Bcrypt) hashPasswordWithParams Int cost Password pass = forall (m :: * -> *) a. MonadIO m => IO a -> m a liftIO forall a b. (a -> b) -> a -> b $ do Salt Bcrypt salt <- forall (m :: * -> *). MonadIO m => m (Salt Bcrypt) newSalt forall (m :: * -> *) a. Monad m => a -> m a return forall a b. (a -> b) -> a -> b $ Int -> Salt Bcrypt -> Password -> PasswordHash Bcrypt hashPasswordWithSalt Int cost Salt Bcrypt salt Password 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 -> PasswordHash Bcrypt -> PasswordCheck checkPassword Password pass (PasswordHash Text passHash) = if forall password hash. (ByteArray password, ByteArray hash) => password -> hash -> Bool Bcrypt.validatePassword (Text -> Bytes toBytes forall a b. (a -> b) -> a -> b $ Password -> Text unsafeShowPassword Password pass) (Text -> Bytes toBytes Text passHash) then PasswordCheck PasswordCheckSuccess else PasswordCheck 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 Bcrypt -> Maybe Int extractParams (PasswordHash Text passHash) = case (Char -> Bool) -> Text -> [Text] T.split (forall a. Eq a => a -> a -> Bool == Char '$') Text passHash of [Text _, Text version, Text cost, Text _pass] -> do forall (f :: * -> *). Alternative f => Bool -> f () guard forall a b. (a -> b) -> a -> b $ forall (t :: * -> *) a. (Foldable t, Eq a) => a -> t a -> Bool elem Text version forall a b. (a -> b) -> a -> b $ forall a b. (a -> b) -> [a] -> [b] map String -> Text T.pack [String "2", String "2a", String "2x", String "2y", String "2b"] forall a. Read a => String -> Maybe a readMaybe forall a b. (a -> b) -> a -> b $ Text -> String T.unpack Text cost [Text] _ -> forall a. Maybe a Nothing -- | Generate a random 16-byte @bcrypt@ salt -- -- @since 2.0.0.0 newSalt :: MonadIO m => m (Salt Bcrypt) newSalt :: forall (m :: * -> *). MonadIO m => m (Salt Bcrypt) newSalt = forall (m :: * -> *) a. MonadIO m => Int -> m (Salt a) Data.Password.Internal.newSalt Int 16